module Pineapples::Actions

Constants

DEFAULT_COLOR
FILE_WITH_RUBY_CODE
TEMPLATE_EXTNAME

Attributes

behaviour[R]

Public Class Methods

new(options) click to toggle source
Calls superclass method
# File lib/pineapples/actions.rb, line 35
def initialize(options)
  super

  @behaviour = options.behaviour || :invoke
end

Public Instance Methods

append_file(path, *args, &block)
Alias for: append_to_file
append_to_file(path, *args, &block) click to toggle source

Append text to a file. Since it depends on insert_into_file, it's reversible.

Parameters

path<String>

path of the file to be changed

data<String>

the data to append to the file, can be also given as a block.

config<Hash>

give :verbose => false to not log the status.

Example

append_to_file 'config/environments/test.rb', 'config.gem "rspec"'

append_to_file 'config/environments/test.rb' do
  'config.gem "rspec"'
end
# File lib/pineapples/actions/insert_into_file.rb, line 24
def append_to_file(path, *args, &block)
  options = args.last.is_a?(Hash) ? args.pop : {}
  options.merge!(before: /\z/)
  insert_into_file(path, *(args << options), &block)
end
Also aliased as: append_file
apply(path, options = {}) click to toggle source

Loads an external file and execute it in the instance binding.

Parameters

path<String>

The path to the file to execute. Can be a web address or a relative path from the source root.

Examples

apply "http://gist.github.com/103208"

apply "recipes/jquery.rb"
# File lib/pineapples/actions/apply.rb, line 15
def apply(path, options = {})
  verbose = options.fetch(:verbose, verbose?)
  is_uri  = path =~ %r{^https?\://}
  path    = find_in_source_paths(path) if !is_uri
  color   = options.fetch(:color, DEFAULT_COLOR)

  say_status(:apply, path, color, verbose)

  indent(verbose) do
    if is_uri
      contents = open(path, 'Accept' => 'application/x-thor-template') { |io| io.read }
    else
      contents = open(path) { |io| io.read }
    end

    instance_eval(contents, path)
  end
end
ask_file_collision(target) click to toggle source
# File lib/pineapples/actions/create_file.rb, line 28
def ask_file_collision(target) # rubocop:disable MethodLength
  return true if @always_force
  options = '[Ynaq]'

  question_string = "Overwrite #{target}?"
  options = ['Yes', 'No', 'Always', 'Quit']
  colors = [:light_green, :light_red, :light_yellow, :light_red]
  options_with_color = options.map.with_index { |option, index| option.send(colors[index])}

  loop do
    answer_index = Ask.list(question_string, options_with_color, {clear: false, response: false})
    answer = options[answer_index]

    case answer
    when 'Yes'
      return true
    when 'No'
      return false
    when 'Always'
      return @always_force = true
    when 'Quit'
      say 'Aborting'
      raise SystemExit
    end
  end
end
bundle(command, options = {}) click to toggle source
# File lib/pineapples/actions/bundle.rb, line 3
def bundle(command, options = {})
  command = "#{command.first.first} #{command.first.last}" if command.is_a?(Hash)
  say_status :bundle, "#{command}"

  _bundle_command = Gem.bin_path('bundler', 'bundle')

  require 'bundler'
  Bundler.with_clean_env do
    output = `"#{Gem.ruby}" "#{_bundle_command}" #{command} #{subcommands}`
    print output if !options[:quiet]
  end
end
chmod(path, mode, options = {}) click to toggle source

Changes the mode of the given file or directory.

Parameters

mode<Integer>

the file mode

path<String>

the name of the file to change mode

options<Hash>

give :verbose => false to not log the status.

Example

chmod "script/server", 0755
# File lib/pineapples/actions/chmod.rb, line 14
def chmod(path, mode, options = {})
  return unless behaviour == :invoke

  verbose = options.fetch(:verbose, verbose?)
  execute = !options.fetch(:pretend, pretend?)
  color   = options.fetch(:color, DEFAULT_COLOR)

  full_path = File.join(app_root, path)
  relative_path = relative_to_app_root(full_path)

  say_status(:chmod, relative_path, color, verbose)

  FileUtils.chmod_R(mode, full_path) if execute
end
convert_directory_to_new_hash_syntax(target, options = {}) click to toggle source
# File lib/pineapples/actions/rails/new_hash_syntax_converter.rb, line 4
def convert_directory_to_new_hash_syntax(target, options = {})
  recursive = options.delete(:recursive) || true

  target_fullpath = File.expand_path(target, app_root)
  target_fullpath = File.join(target_fullpath, '**') if recursive

  excluded_files = Array(options[:exclude])
  exclude_pattern = options[:exclude_pattern]

  files = Dir.glob(target_fullpath, File::FNM_DOTMATCH)
  files.sort.each do |file|
    next if File.directory?(file)
    next if exclude_pattern && file.match(exclude_pattern)
    next if excluded_files.any? do |excluded_file|
      File.basename(excluded_file) == File.basename(file_source)
    end

    case file
    when /#{FILE_WITH_RUBY_CODE}$/
      convert_file_to_new_hash_syntax(file)
    end
  end

end
convert_file_to_new_hash_syntax(path) click to toggle source

Converts file to new Ruby hash syntax, cause that is what I prefer Existing solutions on web usually use perl and shell, but we have power of Ruby and cool gsub_file action at our disposal!

# File lib/pineapples/actions/rails/new_hash_syntax_converter.rb, line 31
def convert_file_to_new_hash_syntax(path)
  regex = /:(\w+)(\s{1})(\s*)=>/
  gsub_file(path, regex, '\1:\3')
end
copy_file(source, *args, &block) click to toggle source

Examples

copy_file "README", "doc/README"

copy_file "doc/README"
# File lib/pineapples/actions/copy_file.rb, line 21
def copy_file(source, *args, &block)
  options = args.last.is_a?(Hash) ? args.pop : {}
  target = args.first || source

  action CopyFile.new(self, source, target, options, &block)
end
create_file(target, *args, &block) click to toggle source

Create a new file relative to the destination root with the given data, which is the return value of a block or a data string.

Parameters

target<String>

the relative path to the destination root.

content<String|NilClass>

the data to append to the file.

options<Hash>

give :verbose => false to not log the status.

Examples

create_file "lib/fun_party.rb" do
  hostname = ask("What is the virtual hostname I should use?")
  "vhost.name = #{hostname}"
end

create_file "config/apache.conf", "your apache config"
# File lib/pineapples/actions/create_file.rb, line 22
def create_file(target, *args, &block)
  options = args.last.is_a?(Hash) ? args.pop : {}
  content = args.first
  action CreateFile.new(self, target, block || content, options)
end
current_app_dir() click to toggle source
# File lib/pineapples/actions/inside.rb, line 7
def current_app_dir
  File.expand_path(File.join(*dir_stack), app_root)
end
dir_stack() click to toggle source
# File lib/pineapples/actions/inside.rb, line 3
def dir_stack
  @dir_stack ||= []
end
directory(source, *args, &block) click to toggle source

Copies recursively the files from source directory to root directory. If any of the files finishes with .tt, it's considered to be a template and is placed in the destination without the extension .tt. If any empty directory is found, it's copied and all .empty_directory files are ignored. If any file name is wrapped within % signs, the text within the % signs will be executed as a method and replaced with the returned value. Let's suppose a doc directory with the following files:

doc/
  components/.empty_directory
  README
  rdoc.rb.tt
  %app_name%.rb

When invoked as:

directory "doc"

It will create a doc directory in the destination with the following files (assuming that the `app_name` method returns the value “blog”):

doc/
  components/
  README
  rdoc.rb
  blog.rb

Parameters

source<String>

the relative path to the source root.

target<String>

the relative path to the destination root.

options<Hash>

give :verbose => false to not log the status.

If :recursive => false, does not look for paths recursively.
If :mode => :preserve, preserve the file mode from the source.
If :exclude_pattern => /regexp/, prevents copying files that match that regexp.

Examples

directory "doc"
directory "doc", "docs", :recursive => false
# File lib/pineapples/actions/directory.rb, line 43
def directory(source, *args, &block)
  options = args.last.is_a?(Hash) ? args.pop : {}
  target = args.first || source
  action Directory.new(self, source, target, options, &block)
end
empty_directory(target, options = {}) click to toggle source

Creates an empty directory.

Parameters

target<String>

the relative path to the app root.

options<Hash>

give :verbose => false to not log the status.

Examples

empty_directory "doc"
# File lib/pineapples/actions/empty_directory.rb, line 13
def empty_directory(target, options = {})
  action EmptyDirectory.new(self, target, options)
end
empty_directory_with_keep_file(target, options = {}) click to toggle source
# File lib/pineapples/actions/keep_file.rb, line 5
def empty_directory_with_keep_file(target, options = {})
  empty_directory(target, options)
  keep_file(target)
end
find_in_source_paths(file) click to toggle source
# File lib/pineapples/actions.rb, line 79
def find_in_source_paths(file)
  files_to_search = [file, file + TEMPLATE_EXTNAME]
  sources = source_paths_for_search

  sources.each do |source_path|
    files_to_search.each do |file|
      source_file = File.expand_path(file, source_path)
      return source_file if File.exist?(source_file)
    end
  end

  message = "Could not find #{file.inspect} in any of your source paths. "

  unless self.templates_root
    message << "Please set your generator instance template_root to PATH with the PATH containing your templates."
  end

  if sources.empty?
    message << "Currently you have no source paths."
  else
    message << "Your current source paths are: \n#{sources.join("\n")}"
  end

  raise Error, message
end
get(source, *args, &block) click to toggle source

Gets the content at the given address and places it at the given relative destination. If a block is given instead of destination, the content of the url is yielded and used as location.

Parameters

source<String>

the address of the given content.

target<String>

the relative path to the destination root.

options<Hash>

give :verbose => false to not log the status.

Examples

get "http://gist.github.com/103208", "doc/README"

get "http://gist.github.com/103208" do |content|
  content.split("\n").first
end
# File lib/pineapples/actions/get.rb, line 22
def get(source, *args, &block)
  options = args.last.is_a?(Hash) ? args.pop : {}
  target = args.first

  source = File.expand_path(find_in_source_paths(source.to_s)) unless source =~ %r{^https?\://}
  render = open(source) { |input| input.binmode.read }

  target ||= if block
               block.arity == 1 ? block.call(render) : block.call
             else
               File.basename(source)
             end

  create_file(target, render, options)
end
git(commands = {}) click to toggle source

Run a command in git.

git :init
git add: "this.file that.rb"
git add: "onefile.rb", rm: "badfile.cxx"
# File lib/pineapples/actions/git.rb, line 8
def git(commands = {})
  if commands.is_a?(Symbol)
    shell "git #{commands}"
  else
    commands.each { |cmd, options| shell "git #{cmd} #{options}" }
  end
end
gsub_file(path, flag, *args, &block) click to toggle source

Run a regular expression replacement on a file.

Parameters

path<String>

path of the file to be changed

flag<Regexp|String>

the regexp or string to be replaced

replacement<String>

the replacement, can be also given as a block

config<Hash>

give :verbose => false to not log the status.

Example

gsub_file 'app/controllers/application_controller.rb', /#\s*(filter_parameter_logging :password)/, '\1'

gsub_file 'README', /rake/, :green do |match|
  match << " no more. Use thor!"
end
# File lib/pineapples/actions/gsub_file.rb, line 19
def gsub_file(path, flag, *args, &block)
  return unless behaviour == :invoke
  options = args.last.is_a?(Hash) ? args.pop : {}

  verbose = options.fetch(:verbose, verbose?)
  execute = options.fetch(:pretend, execute?)

  fullpath = File.expand_path(path, app_root)
  raise Error, "File #{path} doesn't exist!" if !File.exist?(fullpath)

  say_status :gsub, relative_to_app_root(path), :light_yellow, verbose

  if execute
    content = File.binread(fullpath)
    content.gsub!(flag, *args, &block)
    File.open(fullpath, 'wb') { |file| file.write(content) }
  end
end
in_app_root(&block) click to toggle source
# File lib/pineapples/actions/inside.rb, line 53
def in_app_root(&block)
  Dir.chdir(app_root, &block)
end
in_root(options = {}) { || ... } click to toggle source

Convenience method to call inside in_root

# File lib/pineapples/actions/inside.rb, line 49
def in_root(options = {})
  inside(:root, options) { yield }
end
indent(verbose = true, level_increment = 1) { || ... } click to toggle source
# File lib/pineapples/actions.rb, line 123
def indent(verbose = true, level_increment = 1)
  $terminal.indent_level += level_increment if verbose
  yield
  $terminal.indent_level -= level_increment if verbose
end
insert_into_file(target, *args, &block) click to toggle source
# File lib/pineapples/actions/insert_into_file.rb, line 3
def insert_into_file(target, *args, &block)
  content = block_given? ? block : args.shift
  options = args.shift
  action InsertIntoFile.new(self, target, content, options)
end
inside(dir = :root, options = {}) { |target_dir| ... } click to toggle source

Do something in the root or on a provided subfolder. The full path to the directory is yielded to the block you provide. The path is set back to the previous path when the method exits.

Parameters

dir<String>

the directory to move to, relative to the app root

config<Hash>

give :verbose => true to log and use padding.

# File lib/pineapples/actions/inside.rb, line 19
def inside(dir = :root, options = {}, &block)
  verbose = options.fetch(:verbose, verbose?)
  color = options.fetch(:color, :light_green)

  status_dir = if dir == :root
                 relative_to_app_root(app_root)
               else
                 relative_to_app_root(File.join(current_app_dir, dir))
               end

  say_status(:inside, status_dir, color, verbose)

  indent(verbose) do
    with_directory(dir) do |target_dir|
      if !File.exist?(target_dir) && !pretend?
        FileUtils.mkdir_p(target_dir)
      end

      if pretend?
        block.arity == 1 ? yield(target_dir) : yield
      else
        FileUtils.cd(target_dir) do
          block.arity == 1 ? yield(target_dir) : yield
        end
      end
    end
  end
end
keep_file(target) click to toggle source
# File lib/pineapples/actions/keep_file.rb, line 10
def keep_file(target)
  create_file("#{target}/.keep")
end
prepend_file(path, *args, &block)
Alias for: prepend_to_file
prepend_to_class(path, klass, *args, &block) click to toggle source

Injects text right after the class definition. Since it depends on insert_into_file, it's reversible.

Parameters

path<String>

path of the file to be changed

klass<String|Class>

the class to be manipulated

data<String>

the data to append to the class, can be also given as a block.

config<Hash>

give :verbose => false to not log the status.

Examples

prepend_to_class "app/controllers/application_controller.rb", ApplicationController, "  filter_parameter :password\n"

prepend_to_class "app/controllers/application_controller.rb", ApplicationController do
  "  filter_parameter :password\n"
end
# File lib/pineapples/actions/prepend_to_class.rb, line 20
def prepend_to_class(path, klass, *args, &block)
  options = args.last.is_a?(Hash) ? args.pop : {}
  options.merge!(after: /class #{klass}\n|class #{klass} .*\n/)
  insert_into_file(path, *(args << options), &block)
end
prepend_to_file(path, *args, &block) click to toggle source

Prepend text to a file. Since it depends on insert_into_file, it's reversible.

Parameters

path<String>

path of the file to be changed

content<String>

the content to prepend to the file, can be also given as a block.

options<Hash>

give :verbose => false to not log the status.

Example

prepend_to_file 'config/environments/test.rb', 'config.gem "rspec"'

prepend_to_file 'config/environments/test.rb' do
  'config.gem "rspec"'
end
# File lib/pineapples/actions/insert_into_file.rb, line 46
def prepend_to_file(path, *args, &block)
  options = args.last.is_a?(Hash) ? args.pop : {}
  options.merge!(after: /\A/)
  insert_into_file(path, *(args << options), &block)
end
Also aliased as: prepend_file
relative_to_app_root(dir) click to toggle source

dir argument must me absolute path

# File lib/pineapples/actions.rb, line 67
def relative_to_app_root(dir)
  @app_root_pathname ||= Pathname.new(app_root)
  path = Pathname.new(dir)
  path.relative_path_from(@app_root_pathname).to_s
end
relative_to_current_app_dir(dir) click to toggle source
# File lib/pineapples/actions.rb, line 73
def relative_to_current_app_dir(dir)
  current_app_path = Pathname.new(current_app_dir)
  path = Pathname.new(dir)
  path.relative_path_from(current_app_path).to_s
end
remove_dir(path, options = {})
Alias for: remove_file
remove_file(path, options = {}) click to toggle source

Removes a file at the given location.

Parameters

path<String>

path of the file to be changed

options<Hash>

give :verbose => false to not log the status.

Example

remove_file 'README'
remove_file 'app/controllers/application_controller.rb'
# File lib/pineapples/actions/remove_file.rb, line 14
def remove_file(path, options = {})
  return unless behaviour == :invoke
  path  = File.expand_path(path, app_root)

  message = relative_to_app_root(path)
  verbose = options.fetch(:verbose, verbose?)
  color = options.fetch(:color, :light_red)
  execute = options[:pretend] || execute?

  say_status :remove, message, color, verbose
  ::FileUtils.rm_rf(path) if execute && File.exist?(path)
end
Also aliased as: remove_dir
ruby(command, options = {}) click to toggle source

Executes a ruby script (taking into account WIN32 platform quirks).

Parameters

command<String>

the command to be executed.

options<Hash>

give :verbose => false to not log the status.

# File lib/pineapples/actions/ruby.rb, line 10
def ruby(command, options = {})
  return if behaviour != :inboke
  shell(command, options.merge(with: ruby_command))
end
say_status(status, message, color = :light_green, log = true) click to toggle source
# File lib/pineapples/actions.rb, line 105
def say_status(status, message, color = :light_green, log = true)
  return if !log

  spaces = ' ' * $terminal.indent_size * 2

  status = status.to_s.rjust(12)
  status = status.colorize(color)

  output = "#{status}#{spaces}#{message}"

  say(output)
end
say_title(title) click to toggle source
# File lib/pineapples/actions.rb, line 118
def say_title(title)
  puts
  puts "== #{title} ==".light_yellow
end
shell(command, options = {}) click to toggle source

Executes a command returning the contents of the command.

Parameters

command<String>

the command to be executed.

options<Hash>

give :verbose => false to not log the status, :capture => true to hide to output. Specify :with

to append an executable to command execution.

Example

inside 'vendor' do
  shell 'ln -s ~/edge rails'
end
# File lib/pineapples/actions/shell.rb, line 16
def shell(command, options = {})
  return if behaviour == :revoke

  verbose = options.fetch(:verbose, verbose?)
  execute = !options.fetch(:pretend, pretend?)

  executable = options[:with].to_s

  command = "#{executable} #{command}" if executable.present?

  say_status(:shell, command, DEFAULT_COLOR, verbose)

  if execute
    options[:capture] ? `#{command}` : system("#{command}")
    raise Error, "#{command} failed with status #{$CHILD_STATUS.exitstatus}." if not $CHILD_STATUS.success?
  end
end
shell_with_app_gemset(command, options = {}) click to toggle source
# File lib/pineapples/actions/shell.rb, line 41
def shell_with_app_gemset(command, options = {})
  # we have to use PTY pseudo terminal to able to read shell subprocess output asynchronously.
  # Backticks and Kernel#system buffer output, and since bundle install takes forever to complete
  # it's not very user-friendly and program would seem to hang.
  # We just cd into project directory and invoke RVM in binary mode ('do' command)

  # TODO: add support for rbenv gemsets
  return if behaviour == :revoke
  verbose = options.fetch(:verbose, verbose?)
  execute = !options.fetch(:pretend, pretend?)

  full_command = "export DISABLE_SPRING=0; cd #{File.basename(app_root)}; rvm . do #{command}"
  say_status :shell, full_command, DEFAULT_COLOR, verbose

  if execute
    begin
      PTY.spawn(full_command) do |reader, writer, pid|
        loop do
          line = reader.gets
          break if !line
          puts line
        end
        Process.wait(pid)
      end
    rescue Errno::EIO => error
      nil
    rescue PTY::ChildExited => error
      puts 'The child process exited!'
    end
  end
end
shell_with_clean_bundler_env(command, options = {}) click to toggle source
# File lib/pineapples/actions/shell.rb, line 34
def shell_with_clean_bundler_env(command, options = {})
  return shell(command, options) if !defined?(Bundler)
  Bundler.with_clean_env do
    shell(command, options)
  end
end
source_paths() click to toggle source
# File lib/pineapples/actions.rb, line 55
def source_paths
  @source_paths ||= []
end
template(source, *args, &block) click to toggle source

Gets an ERB template at the relative source, executes it and makes a copy at the relative destination. If the destination is not given it's assumed to be equal to the source removing .tt from the filename.

Parameters

source<String>

the relative path to the source root.

target<String>

the relative path to the destination root.

options<Hash>

give :verbose => false to not log the status.

Examples

template "README", "doc/README"

template "doc/README"
# File lib/pineapples/actions/template.rb, line 20
def template(source, *args, &block)
  options = args.last.is_a?(Hash) ? args.pop : {}
  target = args.first || source.sub(/#{TEMPLATE_EXTNAME}$/, '')

  source  = File.expand_path(find_in_source_paths(source.to_s))
  context = options.delete(:context) || binding #instance_eval('binding')

  create_file(target, nil, options) do
    template = File.binread(source)
    content = ERB.new(template, nil, '-').result(context)
    content = block.call(content) if block
    content
  end
end
templates_root() click to toggle source
# File lib/pineapples/actions.rb, line 51
def templates_root
  @templates_root ||= File.join(__dir__, 'templates')
end

Private Instance Methods

ruby_command() click to toggle source

Return the path to the ruby interpreter taking into account multiple installations and windows extensions.

# File lib/pineapples/actions/ruby.rb, line 19
def ruby_command # rubocop:disable MethodLength
  @ruby_command ||= begin
    ruby_name = RbConfig::CONFIG['ruby_install_name']
    ruby = File.join(RbConfig::CONFIG['bindir'], ruby_name)
    ruby << RbConfig::CONFIG['EXEEXT']

    # avoid using different name than ruby (on platforms supporting links)
    if ruby_name != 'ruby' && File.respond_to?(:readlink)
      begin
        alternate_ruby = File.join(RbConfig::CONFIG['bindir'], 'ruby')
        alternate_ruby << RbConfig::CONFIG['EXEEXT']

        # ruby is a symlink
        if File.symlink? alternate_ruby
          linked_ruby = File.readlink alternate_ruby

          # symlink points to 'ruby_install_name'
          ruby = alternate_ruby if linked_ruby == ruby_name || linked_ruby == ruby
        end
      rescue NotImplementedError # rubocop:disable HandleExceptions
        # just ignore on windows
      end
    end

    # escape string in case path to ruby executable contain spaces.
    ruby.sub!(/.*\s.*/m, '"\&"')
    ruby
  end
end
with_directory(dir) { |target_dir| ... } click to toggle source

helper method to abstract directory stack management in the inside action

# File lib/pineapples/actions/inside.rb, line 60
def with_directory(dir)
  not_in_root = (dir != :root)

  dir_stack.push(dir) if not_in_root

  target_dir = not_in_root ? current_app_dir : app_root

  yield target_dir

  dir_stack.pop if not_in_root
end