How to provide config file for Ruby command line utility written in Ruby? - ruby

I have a command line utility written in Ruby using GLI framework. I would like to have configuration for my command line utility in my home directory, using Ruby itself as DSL to handle it (similar to Gemfile or Rakefile).
I have in class ConfigData in folder lib/myapp. The class looks like following way:
class ConfigData
##data = {}
class ConfigItem
def initialize
#data = {}
end
def missing_method(name, *args)
#data[name] = args[0]
end
end
def self.add(section)
item = ConfigItem.new()
yield item
##data[section]=item
end
end
Now, what I would like to have, is the config file, preferrably with name Myappfile, in current working folder, with the following content
add('section1') do |i|
i.param1 'Some data'
i.param2 'More data'
end
When this code was included between class and end of ConfigData, it worked fine. But now I would like to have it placed in the working folder, where I start the application.
I tried require('./Myappfile') between class and end of ConfigData, but it doesn't work for me. I tried to read the source codes of rake, but it is not very much clear to me.
Any hint how this can be solved?

To evaluate code within the context of an instance, which is what you want to do, you need the instance_eval() method. Never, ever, use normal eval. Ever. Anyway, here's how you'd load your fingi file and get the data:
config = ConfigData.new
config.instance_eval(File.read("Myconfig"))
#Access configuration data here from the config object
That simple. After you've loaded the object in that way, you can access values of the object.
WARNING: This is not very secure. This is actually a gaping security hole. Here's the secure version:
f = Fiber.new {str = File.read("Myconfig"); $SAFE = 4; config = ConfigData.new; config.instance_eval(str); Fiber.yield config}
confdata = f.resume
#Access configuration data here from confdata.
This executes the external code in a (sort of) sandbox, so that it can't do anything dastardly.
Also, why don't you just use a YAML config? Unless configuration needs to run code like pwd or access RUBY_VERSION, YAML is much simpler and more secure, in addition to being more failproof.

Related

Anyway to reload the modified gem files in the REPL

While trying to build a Ruby gem (using Bundler), I tend to test the code using the REPL provided by Bundler - accessible via bundle console.
Is there any way to reload the entire project in it? I end up loading individual (changed) files again to test the new change.
The following hack works for a relatively simple gem of mine and Ruby 2.2.2. I'll be curious to see if it works for you. It makes the following assumptions:
You have the conventional folder structure: a file called lib/my_gem_name.rb and a folder lib/my_gem_name/ with any files / folder structure underneath.
All the classes you want to reload are nested within your top module MyGemName.
It will probably not work if in one of the files under lib/my_gem_name/ you monkey-patch classes outside of your MyGemName namespace.
If you're good with the assumptions above, put the following code inside the module definition in lib/my_gem_name.rb and give it a try:
module MyGemName
def self.reload!
Reloader.new(self).reload
end
class Reloader
def initialize(top)
#top = top
end
def reload
cleanup
load_all
end
private
# #return [Array<String>] array of all files that were loaded to memory
# under the lib/my_gem_name folder.
# This code makes assumption #1 above.
def loaded_files
$LOADED_FEATURES.select{|x| x.starts_with?(__FILE__.chomp('.rb'))}
end
# #return [Array<Module>] Recursively find all modules and classes
# under the MyGemName namespace.
# This code makes assumption number #2 above.
def all_project_objects(current = #top)
return [] unless current.is_a?(Module) and current.to_s.split('::').first == #top.to_s
[current] + current.constants.flat_map{|x| all_project_objects(current.const_get(x))}
end
# #return [Hash] of the format {Module => true} containing all modules
# and classes under the MyGemName namespace
def all_project_objects_lookup
#_all_project_objects_lookup ||= Hash[all_project_objects.map{|x| [x, true]}]
end
# Recursively removes all constant entries of modules and classes under
# the MyGemName namespace
def cleanup(parent = Object, current = #top)
return unless all_project_objects_lookup[current]
current.constants.each {|const| cleanup current, current.const_get(const)}
parent.send(:remove_const, current.to_s.split('::').last.to_sym)
end
# Re-load all files that were previously reloaded
def load_all
loaded_files.each{|x| load x}
true
end
end
end
If you don't want this functionality to be available in production, consider monkey-patching this in the bin/console script, but make sure to change the line $LOADED_FEATURES.select{|x| x.starts_with?(__FILE__.chomp('.rb'))} to something that will return a list of relevant loaded files given the new location of the code.
If you have a standard gem structure, this should work:
$LOADED_FEATURES.select{|x| x.starts_with?(File.expand_path('../../lib/my_gem_name'))} (make sure to put your monkey patching code before the IRB.start or Pry.start)

Where to put helper functions for rake tasks and test files in Ruby on Rails?

In my Rails application I have a file sample_data.rb inside /lib/tasks as well as a bunch of test files inside my /spec directory.
All these files often share common functionality such as:
def random_address
[Faker::Address.street_address, Faker::Address.city].join("\n")
end
Where should I put those helper functions? Is there some sort of convention on this?
Thanks for any help!
You could create a static class, with static functions. That would look something like this:
class HelperFunctions
def self.random_address
[Faker::Address.street_address, Faker::Address.city].join("\n")
end
def self.otherFunction
end
end
Then, all you would need to do is:
include your helper class in the file you want to use
execute it like:
HelperFunctions::random_address(anyParametersYouMightHave)
When doing this, make sure you include any dependencies in your HelperFunctions class.
If you're sure it's rake only specific, you also can add in directly in RAILS_ROOT/Rakefile (that's probably not the case for the example you use).
I use this to simplify rake's invoke syntax :
#!/usr/bin/env rake
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require File.expand_path('../config/application', __FILE__)
def invoke( task_name )
Rake::Task[ task_name ].invoke
end
MyApp::Application.load_tasks
That way, I can use invoke "my_namespace:my_task" in rake tasks instead of Rake::Task[ "my_namespace:my_task" ].invoke.
You share methods in a module, and you place such a module inside the lib folder.
Something like lib/fake_data.rb containing
module FakeData
def random_address
[Faker::Address.street_address, Faker::Address.city].join("\n")
end
module_function
end
and inside your rake task just require the module, and call FakeData.random_address.
But, if it is like a seed you need to do every time you run your tests, you should consider adding this to your general before all.
E.g. my spec_helper looks like this:
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
RSpec.configure do |config|
config.use_transactional_fixtures = true
config.infer_base_class_for_anonymous_controllers = false
config.order = "random"
include SetupSupport
config.before(:all) do
load_db_seed
end
end
and the module SetupSupport is defined in spec/support/setup_support.rb and looks as follows:
module SetupSupport
def load_db_seed
load(File.join(Rails.root, 'db', 'seeds.rb'))
end
end
Not sure if you need to load the seeds, or are already doing this, but this is the ideal spot to also generate needed fake data.
Note that my setup support class is defined in spec/support because the code is only relevant to my specs, I have no rake task also needing the same code.

How can I make Aruba use a different ENV["HOME"]?

From the docs:
Per default Aruba will create a directory tmp/aruba where it performs its file operations.
However, my application uses ENV["HOME"] to create and read a file (~/.foorc), so I need Aruba to use a fake ENV["HOME"].
Do I need to set it in some support-file, or is there a way to tell Aruba to its tmp/aruba for files in ENV["HOME"]?
Here is an excerpt of my code that I am testing (obviously I am testing this with Cucumber/Aruba on a much higher level, but the usage of ENV["HOME"] is what is important here.):
def initialize config_path = ""
if config_path.empty?
#config_path = File.join ENV["HOME"], ".todotxt.cfg"
else
#config_path = config_path
end
if file_exists?
super #config_path
validate
end
end
def file_exists?
File.exists? #config_path
end
#....
ask_to_create unless #config.file_exists?
#...
The Specification:
Scenario: todotxt
Given an empty installation
When I run `todotxt`
Then it should pass with:
"""
Should I create a sample config file? [Y/n]
"""
Looking into the implementation in Aruba itself, I could craft something very similar:
File features/support/aruba.rb, is autoloaded by cucumber and implements the Around hook:
# Temporarily enforce an isolated, fake, homedir.
around do |scenario, block|
#__aruba_original_home = ENV["HOME"]
ENV["HOME"] = File.expand_path(File.join("tmp", "aruba"))
block.call
ENV["HOME"] = #__aruba_original_home
end
From now on, a directory tmp/aruba is used as $HOME.
Note that in aruba, this temporary path is configurable, and that above code does not take that into consideration. It will break when the tmp path is configured elsewhere.
Aruba offers a step for just that:
Given a mocked home directory

How to test a script that generates files

I am creating a Rubygem that will let me generate jekyll post files. One of the reasons I am developing this project is to learn TDD. This gem is strictly functional on the command line, and it has to make a series of checks to make sure that it finds the _posts directory. This depends on two things:
Wether or not a location option was passed
Is that location option valid?
A location option was not passed
Is the posts dir in the current directory?
Is the posts dir the current working directory?
At that point, I am really having a hard time testing that part of the application. So I have two questions:
is it acceptable/okay to skip tests for small parts of the application like the one described above?
If not, how do you test file manipulation in ruby using minitest?
Some projects I've seen implement their command line tools as Command objects (for example: Rubygems and my linebreak gem). These objects are initialized with the ARGV simply have a call or execute method which then starts the whole process. This enables these projects to put their command line applications into a virtual environment. They could, for example hold the input and output stream objects in instance variables of the command object to make the application independant of using STDOUT/STDIN. And thus, making it possible to test the input/output of the command line application. In the same way I imagine, you could hold your current working directory in an instance variable to make your command line application independent of your real working directory. You could then create a temporary directory for each test and set this one as the working directory for your Command object.
And now some code:
require 'pathname'
class MyCommand
attr_accessor :input, :output, :error, :working_dir
def initialize(options = {})
#input = options[:input] ? options[:input] : STDIN
#output = options[:output] ? options[:output] : STDOUT
#error = options[:error] ? options[:error] : STDERR
#working_dir = options[:working_dir] ? Pathname.new(options[:working_dir]) : Pathname.pwd
end
# Override the puts method to use the specified output stream
def puts(output = nil)
#output.puts(output)
end
def execute(arguments = ARGV)
# Change to the given working directory
Dir.chdir(working_dir) do
# Analyze the arguments
if arguments[0] == '--readfile'
posts_dir = Pathname.new('posts')
my_file = posts_dir + 'myfile'
puts my_file.read
end
end
end
end
# Start the command without mockups if the ruby script is called directly
if __FILE__ == $PROGRAM_NAME
MyCommand.new.execute
end
Now in your test's setup and teardown methods you could do:
require 'pathname'
require 'tmpdir'
require 'stringio'
def setup
#working_dir = Pathname.new(Dir.mktmpdir('mycommand'))
#output = StringIO.new
#error = StringIO.new
#command = MyCommand.new(:working_dir => #working_dir, :output => #output, :error => #error)
end
def test_some_stuff
#command.execute(['--readfile'])
# ...
end
def teardown
#working_dir.rmtree
end
(In the example I'm using Pathname, which is a really nice object oriented file system API from Ruby's standard library and StringIO, which is useful for for mocking STDOUT as it's an IO object which streams into a simple String)
In the acutal test you could now use the #working_dir variable to test for existence or content of files:
path = #working_dir + 'posts' + 'myfile'
path.exist?
path.file?
path.directory?
path.read == "abc\n"
From my experience (and thus this is VERY subjective), I think it's ok sometimes to skip unit testing in some areas which are difficult to test. You need to find out what you get in return and the cost for testing or not. My rule of thumb is that the decision to not test a class should be very unusual (around less than 1 in 300 classes)
If what you're trying to test is very difficult, because of the dependencies with the file system, I think you could try to extract all the bits that interact with the file system.

Can I disable the log header for ruby logger?

I'm currently running into kind of a problem.
As you might know, the ruby logger adds a logging header at the top of every newly created logfile.
"# Logfile created on %s by %s\n" % [Time.now.to_s, Logger::ProgName]
I am logging CSV files to import them in a warehouse later, usually I just skip the first line with the header. It's seems like there is a bug in the logger, because sometimes the logging header appears more than once, right in the middle of a log file.
So I decided to simply leave that header out. To my surprise I didn't find any argument one could pass at the creation of a logger. I thought of something like this:
Logger.new "info.log", :skip_header => true
But it's just not there. I searched in the ruby core sources and surprisingly there really is nothing that could prevent the logger from adding the log header:
def create_logfile(filename)
logdev = open(filename, (File::WRONLY | File::APPEND | File::CREAT))
logdev.sync = true
add_log_header(logdev)
logdev
end
def add_log_header(file)
file.write(
"# Logfile created on %s by %s\n" % [Time.now.to_s, Logger::ProgName]
)
end
Does anyone have an idea what I could do, to prevent the log header? I'm using Ruby 1.8.7 302 with Rails 2.3.5 here. Simply ignoring the comments on the warehouse side is not possible because I have no control over the code there, and it seems to be to risky to simply ignore it, if something goes wrong with a a logging line.
Does someone know a logger that allows this? Do you think it would be a good idea to use and write plain to a file?
Thanks in advance,
Tommy
Ideally the method add_log_header on the Logger instance should be overwritten, but since add_log_header is called on initialize, you're too late by the time you get your hands on it.
Well, you could just overwrite the add_log_header method on the Class.
class Logger::LogDevice
def add_log_header(file)
end
end
log1 = Logger.new('info1.log')
But if your app needs more instances of Logger after this, they will behave the same: no header. To prevent this:
# dismantle the header and save it under another name
class Logger::LogDevice
alias orig_add_log_header add_log_header
def add_log_header(file)
end
end
# Quick,create an instance
log1 = Logger.new('test_log1file.log')
# restore the old method:
class Logger::LogDevice
alias add_log_header orig_add_log_header
end
Here's a solution that involves subclassing Logger. We have to be sneaky with initialize and super to keep it from creating a standard Logger::LogDevice too early.
class HeadlessLogger < Logger
def initialize(logdev, shift_age = 0, shift_size = 1048576)
super(nil) # this prevents it from initializing a LogDevice
if logdev
#logdev = HeadlessLogger::LogDevice.new(logdev, shift_age: shift_age, shift_size: shift_size)
end
end
class LogDevice < ::Logger::LogDevice
def add_log_header(file) ; end
end
end
As an alternative to patching the logger class, simply do not let it create the log file by touching it upfront:
FileUtils.touch logfile_path
Logger.new logfile_path
In plain Ruby you will need to require 'fileutils' from stdlib obviously.
Edit: This will not work if you use the built-in logrotation though, or the file is deleted, as there is no on-rotate hook and it will then write the header yet again.

Resources