From 6813edc7d926965e5644cd8befaf229a35b9d8ca Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Sat, 28 May 2016 22:04:13 +0200 Subject: [PATCH] Initial command structure. --- railties/lib/rails/cli.rb | 6 +- railties/lib/rails/command.rb | 96 ++++++++ railties/lib/rails/command/actions.rb | 18 ++ railties/lib/rails/command/base.rb | 130 ++++++++++ railties/lib/rails/command/behavior.rb | 123 ++++++++++ .../lib/rails/command/environment_argument.rb | 32 +++ railties/lib/rails/commands.rb | 6 +- .../application/application_command.rb | 29 +++ .../rails/commands/console/console_command.rb | 89 +++++++ .../commands/dbconsole/dbconsole_command.rb | 160 ++++++++++++ .../rails/commands/destroy/destroy_command.rb | 21 ++ .../commands/generate/generate_command.rb | 21 ++ railties/lib/rails/commands/help/USAGE | 16 ++ .../lib/rails/commands/help/help_command.rb | 13 + .../lib/rails/commands/new/new_command.rb | 15 ++ .../rails/commands/plugin/plugin_command.rb | 43 ++++ .../lib/rails/commands/rake/rake_command.rb | 49 ++++ railties/lib/rails/commands/runner/USAGE | 17 ++ .../rails/commands/runner/runner_command.rb | 45 ++++ .../rails/commands/server/server_command.rb | 159 ++++++++++++ .../lib/rails/commands/test/test_command.rb | 18 ++ .../rails/commands/version/version_command.rb | 9 + railties/lib/rails/generators.rb | 230 ++++++------------ .../lib/rails/test_unit/minitest_plugin.rb | 1 + railties/test/application/runner_test.rb | 2 +- 25 files changed, 1182 insertions(+), 166 deletions(-) create mode 100644 railties/lib/rails/command.rb create mode 100644 railties/lib/rails/command/actions.rb create mode 100644 railties/lib/rails/command/base.rb create mode 100644 railties/lib/rails/command/behavior.rb create mode 100644 railties/lib/rails/command/environment_argument.rb create mode 100644 railties/lib/rails/commands/application/application_command.rb create mode 100644 railties/lib/rails/commands/console/console_command.rb create mode 100644 railties/lib/rails/commands/dbconsole/dbconsole_command.rb create mode 100644 railties/lib/rails/commands/destroy/destroy_command.rb create mode 100644 railties/lib/rails/commands/generate/generate_command.rb create mode 100644 railties/lib/rails/commands/help/USAGE create mode 100644 railties/lib/rails/commands/help/help_command.rb create mode 100644 railties/lib/rails/commands/new/new_command.rb create mode 100644 railties/lib/rails/commands/plugin/plugin_command.rb create mode 100644 railties/lib/rails/commands/rake/rake_command.rb create mode 100644 railties/lib/rails/commands/runner/USAGE create mode 100644 railties/lib/rails/commands/runner/runner_command.rb create mode 100644 railties/lib/rails/commands/server/server_command.rb create mode 100644 railties/lib/rails/commands/test/test_command.rb create mode 100644 railties/lib/rails/commands/version/version_command.rb diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb index 26ef3822ba..973b746068 100644 --- a/railties/lib/rails/cli.rb +++ b/railties/lib/rails/cli.rb @@ -7,9 +7,11 @@ Rails::AppLoader.exec_app require "rails/ruby_version_check" Signal.trap("INT") { puts; exit(1) } +require "rails/command" + if ARGV.first == "plugin" ARGV.shift - require "rails/commands/plugin" + Rails::Command.invoke :plugin, ARGV else - require "rails/commands/application" + Rails::Command.invoke :application, ARGV end diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb new file mode 100644 index 0000000000..22eaf37bf7 --- /dev/null +++ b/railties/lib/rails/command.rb @@ -0,0 +1,96 @@ +require "active_support" +require "active_support/core_ext/enumerable" +require "active_support/core_ext/object/blank" +require "active_support/core_ext/hash/transform_values" + +require "thor" + +module Rails + module Command + autoload :Behavior, "rails/command/behavior" + autoload :Base, "rails/command/base" + + include Behavior + + class << self + def hidden_commands # :nodoc: + @hidden_commands ||= [] + end + + def environment + ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" + end + + # Receives a namespace, arguments and the behavior to invoke the command. + def invoke(namespace, args = [], **config) + namespace = namespace.to_s + namespace = "help" if namespace.blank? || Thor::HELP_MAPPINGS.include?(namespace) + namespace = "version" if %w( -v --version ).include? namespace + + if command = find_by_namespace(namespace) + command.perform(namespace, args, config) + else + find_by_namespace("rake").perform(namespace, args, config) + end + end + + # Rails finds namespaces similar to thor, it only adds one rule: + # + # Command names must end with "_command.rb". This is required because Rails + # looks in load paths and loads the command just before it's going to be used. + # + # find_by_namespace :webrat, :rails, :integration + # + # Will search for the following commands: + # + # "rails:webrat", "webrat:integration", "webrat" + # + # Notice that "rails:commands:webrat" could be loaded as well, what + # Rails looks for is the first and last parts of the namespace. + def find_by_namespace(name) # :nodoc: + lookups = [ name, "rails:#{name}" ] + + lookup(lookups) + + namespaces = subclasses.index_by(&:namespace) + namespaces[(lookups & namespaces.keys).first] + end + + # Returns the root of the Rails engine or app running the command. + def root + if defined?(ENGINE_ROOT) + Pathname.new(ENGINE_ROOT) + elsif defined?(APP_PATH) + Pathname.new(File.expand_path("../..", APP_PATH)) + end + end + + def print_commands # :nodoc: + sorted_groups.each { |b, n| print_list(b, n) } + end + + def sorted_groups # :nodoc: + lookup! + + groups = (subclasses - hidden_commands).group_by { |c| c.namespace.split(":").first } + groups.transform_values! { |commands| commands.flat_map(&:printing_commands).sort } + + rails = groups.delete("rails") + [[ "rails", rails ]] + groups.sort.to_a + end + + protected + def command_type + @command_type ||= "command" + end + + def lookup_paths + @lookup_paths ||= %w( rails/commands commands ) + end + + def file_lookup_paths + @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_command.rb" ] + end + end + end +end diff --git a/railties/lib/rails/command/actions.rb b/railties/lib/rails/command/actions.rb new file mode 100644 index 0000000000..52d3348db3 --- /dev/null +++ b/railties/lib/rails/command/actions.rb @@ -0,0 +1,18 @@ +module Rails + module Command + module Actions + private + # Change to the application's path if there is no config.ru file in current directory. + # This allows us to run `rails server` from other directories, but still get + # the main config.ru and properly set the tmp directory. + def set_application_directory! + Dir.chdir(File.expand_path("../../", APP_PATH)) unless File.exist?(File.expand_path("config.ru")) + end + + def require_application_and_environment! + require APP_PATH + Rails.application.require_environment! + end + end + end +end diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb new file mode 100644 index 0000000000..2ef8393863 --- /dev/null +++ b/railties/lib/rails/command/base.rb @@ -0,0 +1,130 @@ +require "thor" +require "erb" + +require "active_support/core_ext/string/filters" +require "active_support/core_ext/string/inflections" + +require "rails/command/actions" + +module Rails + module Command + class Base < Thor + class Error < Thor::Error # :nodoc: + end + + include Actions + + class << self + # Tries to get the description from a USAGE file one folder above the command + # root. + def desc(usage = nil, description = nil) + if usage + super + else + @desc ||= ERB.new(File.read(usage_path)).result(binding) if usage_path + end + end + + # Convenience method to get the namespace from the class name. It's the + # same as Thor default except that the Command at the end of the class + # is removed. + def namespace(name = nil) + if name + super + else + @namespace ||= super.chomp("_command").sub(/:command:/, ":") + end + end + + # Convenience method to hide this command from the available ones when + # running rails command. + def hide_command! + Rails::Command.hidden_commands << self + end + + def inherited(base) #:nodoc: + super + + if base.name && base.name !~ /Base$/ + Rails::Command.subclasses << base + end + end + + def perform(command, args, config) # :nodoc: + command = nil if Thor::HELP_MAPPINGS.include?(args.first) + + dispatch(command, args.dup, nil, config) + end + + def printing_commands + namespace.sub(/^rails:/, "") + end + + def executable + "bin/rails #{command_name}" + end + + # Use Rails' default banner. + def banner(*) + "#{executable} #{arguments.map(&:usage).join(' ')} [options]".squish! + end + + # Sets the base_name taking into account the current class namespace. + # + # Rails::Command::TestCommand.base_name # => 'rails' + def base_name + @base_name ||= begin + if base = name.to_s.split("::").first + base.underscore + end + end + end + + # Return command name without namespaces. + # + # Rails::Command::TestCommand.command_name # => 'test' + def command_name + @command_name ||= begin + if command = name.to_s.split("::").last + command.chomp!("Command") + command.underscore + end + end + end + + # Path to lookup a USAGE description in a file. + def usage_path + if default_command_root + path = File.join(default_command_root, "USAGE") + path if File.exist?(path) + end + end + + # Default file root to place extra files a command might need, placed + # one folder above the command file. + # + # For a `Rails::Command::TestCommand` placed in `rails/command/test_command.rb` + # would return `rails/test`. + def default_command_root + path = File.expand_path(File.join(base_name, command_name), __dir__) + path if File.exist?(path) + end + + private + # Allow the command method to be called perform. + def create_command(meth) + if meth == "perform" + alias_method command_name, meth + else + # Prevent exception about command without usage. + # Some commands define their documentation differently. + @usage ||= "" + @desc ||= "" + + super + end + end + end + end + end +end diff --git a/railties/lib/rails/command/behavior.rb b/railties/lib/rails/command/behavior.rb new file mode 100644 index 0000000000..1d7bab6ab2 --- /dev/null +++ b/railties/lib/rails/command/behavior.rb @@ -0,0 +1,123 @@ +require "active_support" + +module Rails + module Command + module Behavior #:nodoc: + extend ActiveSupport::Concern + + class_methods do + # Remove the color from output. + def no_color! + Thor::Base.shell = Thor::Shell::Basic + end + + # Track all command subclasses. + def subclasses + @subclasses ||= [] + end + + protected + + # This code is based directly on the Text gem implementation. + # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher. + # + # Returns a value representing the "cost" of transforming str1 into str2 + def levenshtein_distance(str1, str2) + s = str1 + t = str2 + n = s.length + m = t.length + + return m if (0 == n) + return n if (0 == m) + + d = (0..m).to_a + x = nil + + # avoid duplicating an enumerable object in the loop + str2_codepoint_enumerable = str2.each_codepoint + + str1.each_codepoint.with_index do |char1, i| + e = i+1 + + str2_codepoint_enumerable.with_index do |char2, j| + cost = (char1 == char2) ? 0 : 1 + x = [ + d[j+1] + 1, # insertion + e + 1, # deletion + d[j] + cost # substitution + ].min + d[j] = e + e = x + end + + d[m] = x + end + + x + end + + # Prints a list of generators. + def print_list(base, namespaces) #:nodoc: + return if namespaces.empty? + puts "#{base.camelize}:" + + namespaces.each do |namespace| + puts(" #{namespace}") + end + + puts + end + + # Receives namespaces in an array and tries to find matching generators + # in the load path. + def lookup(namespaces) #:nodoc: + paths = namespaces_to_paths(namespaces) + + paths.each do |raw_path| + lookup_paths.each do |base| + path = "#{base}/#{raw_path}_#{command_type}" + + begin + require path + return + rescue LoadError => e + raise unless e.message =~ /#{Regexp.escape(path)}$/ + rescue Exception => e + warn "[WARNING] Could not load #{command_type} #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}" + end + end + end + end + + # This will try to load any command in the load path to show in help. + def lookup! #:nodoc: + $LOAD_PATH.each do |base| + Dir[File.join(base, *file_lookup_paths)].each do |path| + begin + path = path.sub("#{base}/", "") + require path + rescue Exception + # No problem + end + end + end + end + + # Convert namespaces to paths by replacing ":" for "/" and adding + # an extra lookup. For example, "rails:model" should be searched + # in both: "rails/model/model_generator" and "rails/model_generator". + def namespaces_to_paths(namespaces) #:nodoc: + paths = [] + namespaces.each do |namespace| + pieces = namespace.split(":") + paths << pieces.dup.push(pieces.last).join("/") + paths << pieces.join("/") + end + paths.uniq! + paths + end + end + end + end +end diff --git a/railties/lib/rails/command/environment_argument.rb b/railties/lib/rails/command/environment_argument.rb new file mode 100644 index 0000000000..d4221d142b --- /dev/null +++ b/railties/lib/rails/command/environment_argument.rb @@ -0,0 +1,32 @@ +require "active_support" + +module Rails + module Command + module EnvironmentArgument #:nodoc: + extend ActiveSupport::Concern + + included do + argument :environment, optional: true, banner: "environment" + end + + private + def extract_environment_option_from_argument + if environment + self.options = options.merge(environment: acceptable_environment(environment)) + end + end + + def acceptable_environment(env = nil) + if available_environments.include? env + env + else + %w(production development test).detect { |e| e =~ /^#{env}/ } || env + end + end + + def available_environments + Dir["config/environments/*.rb"].map { |fname| File.basename(fname, ".*") } + end + end + end +end diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index d64b355aec..fff0119c65 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -1,4 +1,4 @@ -ARGV << "--help" if ARGV.empty? +require "rails/command" aliases = { "g" => "generate", @@ -13,6 +13,4 @@ aliases = { command = ARGV.shift command = aliases[command] || command -require "rails/commands/commands_tasks" - -Rails::CommandsTasks.new(ARGV).run_command!(command) +Rails::Command.invoke command, ARGV diff --git a/railties/lib/rails/commands/application/application_command.rb b/railties/lib/rails/commands/application/application_command.rb new file mode 100644 index 0000000000..7e3a2b011d --- /dev/null +++ b/railties/lib/rails/commands/application/application_command.rb @@ -0,0 +1,29 @@ +require "rails/generators" +require "rails/generators/rails/app/app_generator" + +module Rails + module Generators + class AppGenerator # :nodoc: + # We want to exit on failure to be kind to other libraries + # This is only when accessing via CLI + def self.exit_on_failure? + true + end + end + end + + module Command + class ApplicationCommand < Base + hide_command! + + def help + perform # Punt help output to the generator. + end + + def perform(*args) + Rails::Generators::AppGenerator.start \ + Rails::Generators::ARGVScrubber.new(args).prepare! + end + end + end +end diff --git a/railties/lib/rails/commands/console/console_command.rb b/railties/lib/rails/commands/console/console_command.rb new file mode 100644 index 0000000000..730b108d8c --- /dev/null +++ b/railties/lib/rails/commands/console/console_command.rb @@ -0,0 +1,89 @@ +require "irb" +require "irb/completion" + +require "rails/command/environment_argument" + +module Rails + class Console + module BacktraceCleaner + def filter_backtrace(bt) + if result = super + Rails.backtrace_cleaner.filter([result]).first + end + end + end + + def self.start(*args) + new(*args).start + end + + attr_reader :options, :app, :console + + def initialize(app, options = {}) + @app = app + @options = options + + app.sandbox = sandbox? + app.load_console + + @console = app.config.console || IRB + + if @console == IRB + IRB::WorkSpace.prepend(BacktraceCleaner) + end + end + + def sandbox? + options[:sandbox] + end + + def environment + options[:environment] + end + alias_method :environment?, :environment + + def set_environment! + Rails.env = environment + end + + def start + set_environment! if environment? + + if sandbox? + puts "Loading #{Rails.env} environment in sandbox (Rails #{Rails.version})" + puts "Any modifications you make will be rolled back on exit" + else + puts "Loading #{Rails.env} environment (Rails #{Rails.version})" + end + + if defined?(console::ExtendCommandBundle) + console::ExtendCommandBundle.include(Rails::ConsoleMethods) + end + console.start + end + end + + module Command + class ConsoleCommand < Base + include EnvironmentArgument + + class_option :sandbox, aliases: "-s", type: :boolean, default: false, + desc: "Rollback database modifications on exit." + + class_option :environment, aliases: "-e", type: :string, default: Rails::Command.environment, + desc: "Specifies the environment to run this console under (test/development/production)." + + def perform + extract_environment_option_from_argument + + # RAILS_ENV needs to be set before config/application is required. + ENV["RAILS_ENV"] = options[:environment] + + ARGV.clear # Clear ARGV so IRB doesn't freak. + + require_application_and_environment! + Rails::Console.start(Rails.application, options) + end + end + end +end diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb new file mode 100644 index 0000000000..021714f61d --- /dev/null +++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb @@ -0,0 +1,160 @@ +require "erb" +require "yaml" + +require "rails/command/environment_argument" + +module Rails + class DBConsole + attr_reader :arguments + + def self.start(*args) + new(*args).start + end + + def initialize(arguments = ARGV, options = {}) + @arguments, @options = arguments, options + end + + def start + ENV["RAILS_ENV"] = @options[:environment] || environment + + case config["adapter"] + when /^(jdbc)?mysql/ + args = { + "host" => "--host", + "port" => "--port", + "socket" => "--socket", + "username" => "--user", + "encoding" => "--default-character-set", + "sslca" => "--ssl-ca", + "sslcert" => "--ssl-cert", + "sslcapath" => "--ssl-capath", + "sslcipher" => "--ssl-cipher", + "sslkey" => "--ssl-key" + }.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact + + if config["password"] && @options["include_password"] + args << "--password=#{config['password']}" + elsif config["password"] && !config["password"].to_s.empty? + args << "-p" + end + + args << config["database"] + + find_cmd_and_exec(["mysql", "mysql5"], *args) + + when /^postgres|^postgis/ + ENV["PGUSER"] = config["username"] if config["username"] + ENV["PGHOST"] = config["host"] if config["host"] + ENV["PGPORT"] = config["port"].to_s if config["port"] + ENV["PGPASSWORD"] = config["password"].to_s if config["password"] && @options["include_password"] + find_cmd_and_exec("psql", config["database"]) + + when "sqlite3" + args = [] + + args << "-#{@options['mode']}" if @options["mode"] + args << "-header" if @options["header"] + args << File.expand_path(config["database"], Rails.respond_to?(:root) ? Rails.root : nil) + + find_cmd_and_exec("sqlite3", *args) + + when "oracle", "oracle_enhanced" + logon = "" + + if config["username"] + logon = config["username"] + logon << "/#{config['password']}" if config["password"] && @options["include_password"] + logon << "@#{config['database']}" if config["database"] + end + + find_cmd_and_exec("sqlplus", logon) + + when "sqlserver" + args = [] + + args += ["-D", "#{config['database']}"] if config["database"] + args += ["-U", "#{config['username']}"] if config["username"] + args += ["-P", "#{config['password']}"] if config["password"] + + if config["host"] + host_arg = "#{config['host']}" + host_arg << ":#{config['port']}" if config["port"] + args += ["-S", host_arg] + end + + find_cmd_and_exec("sqsh", *args) + + else + abort "Unknown command-line client for #{config['database']}." + end + end + + def config + @config ||= begin + if configurations[environment].blank? + raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}" + else + configurations[environment] + end + end + end + + def environment + Rails.respond_to?(:env) ? Rails.env : Rails::Command.environment + end + + protected + def configurations + require APP_PATH + ActiveRecord::Base.configurations = Rails.application.config.database_configuration + ActiveRecord::Base.configurations + end + + def find_cmd_and_exec(commands, *args) + commands = Array(commands) + + dirs_on_path = ENV["PATH"].to_s.split(File::PATH_SEPARATOR) + unless (ext = RbConfig::CONFIG["EXEEXT"]).empty? + commands = commands.map { |cmd| "#{cmd}#{ext}" } + end + + full_path_command = nil + found = commands.detect do |cmd| + dirs_on_path.detect do |path| + full_path_command = File.join(path, cmd) + File.file?(full_path_command) && File.executable?(full_path_command) + end + end + + if found + exec full_path_command, *args + else + abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") + end + end + end + + module Command + class DbconsoleCommand < Base + include EnvironmentArgument + + class_option :include_password, aliases: "-p", type: :boolean, + desc: "Automatically provide the password from database.yml" + + class_option :mode, enum: %w( html list line column ), type: :string, + desc: "Automatically put the sqlite3 database in the specified mode (html, list, line, column)." + + class_option :header, type: :string + + class_option :environment, aliases: "-e", type: :string, default: Rails::Command.environment, + desc: "Specifies the environment to run this console under (test/development/production)." + + def perform + extract_environment_option_from_argument + + Rails::DBConsole.start(args, options) + end + end + end +end diff --git a/railties/lib/rails/commands/destroy/destroy_command.rb b/railties/lib/rails/commands/destroy/destroy_command.rb new file mode 100644 index 0000000000..5e6b7f9371 --- /dev/null +++ b/railties/lib/rails/commands/destroy/destroy_command.rb @@ -0,0 +1,21 @@ +require "rails/generators" + +module Rails + module Command + class DestroyCommand < Base + def help # :nodoc: + Rails::Generators.help self.class.command_name + end + + def perform(*) + generator = args.shift + return help unless generator + + require_application_and_environment! + Rails.application.load_generators + + Rails::Generators.invoke generator, args, behavior: :revoke, destination_root: Rails.root + end + end + end +end diff --git a/railties/lib/rails/commands/generate/generate_command.rb b/railties/lib/rails/commands/generate/generate_command.rb new file mode 100644 index 0000000000..b381ca85b9 --- /dev/null +++ b/railties/lib/rails/commands/generate/generate_command.rb @@ -0,0 +1,21 @@ +require "rails/generators" + +module Rails + module Command + class GenerateCommand < Base + def help # :nodoc: + Rails::Generators.help self.class.command_name + end + + def perform(*) + generator = args.shift + return help unless generator + + require_application_and_environment! + load_generators + + Rails::Generators.invoke generator, args, behavior: :invoke, destination_root: Rails::Command.root + end + end + end +end diff --git a/railties/lib/rails/commands/help/USAGE b/railties/lib/rails/commands/help/USAGE new file mode 100644 index 0000000000..0f6ff756ce --- /dev/null +++ b/railties/lib/rails/commands/help/USAGE @@ -0,0 +1,16 @@ +Usage: bin/rails COMMAND [args] [options] + +The most common rails commands are: + generate Generate new code (short-cut alias: "g") + console Start the Rails console (short-cut alias: "c") + server Start the Rails server (short-cut alias: "s") + test Run tests (short-cut alias: "t") + dbconsole Start a console for the database specified in config/database.yml + (short-cut alias: "db") + new Create a new Rails application. "rails new my_app" creates a + new application called MyApp in "./my_app" + +All commands can be run with -h (or --help) for more information. + +In addition to those commands, there are: + diff --git a/railties/lib/rails/commands/help/help_command.rb b/railties/lib/rails/commands/help/help_command.rb new file mode 100644 index 0000000000..5bcc4c8eee --- /dev/null +++ b/railties/lib/rails/commands/help/help_command.rb @@ -0,0 +1,13 @@ +module Rails + module Command + class HelpCommand < Base + hide_command! + + def help(*) + puts self.class.desc + + Rails::Command.print_commands + end + end + end +end diff --git a/railties/lib/rails/commands/new/new_command.rb b/railties/lib/rails/commands/new/new_command.rb new file mode 100644 index 0000000000..13eedfc479 --- /dev/null +++ b/railties/lib/rails/commands/new/new_command.rb @@ -0,0 +1,15 @@ +module Rails + module Command + class NewCommand < Base + def help + Rails::Command.invoke :application, [ "--help" ] + end + + def perform(*) + puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n" + puts "Type 'rails' for help." + exit 1 + end + end + end +end diff --git a/railties/lib/rails/commands/plugin/plugin_command.rb b/railties/lib/rails/commands/plugin/plugin_command.rb new file mode 100644 index 0000000000..d6d9fe4400 --- /dev/null +++ b/railties/lib/rails/commands/plugin/plugin_command.rb @@ -0,0 +1,43 @@ +module Rails + module Command + class PluginCommand < Base + hide_command! + + def help + run_plugin_generator %w( --help ) + end + + def self.banner(*) # :nodoc: + "#{executable} new [options]" + end + + class_option :rc, type: :boolean, default: File.join("~", ".railsrc"), + desc: "Initialize the plugin command with previous defaults. Uses .railsrc in your home directory by default." + + class_option :no_rc, desc: "Skip evaluating .railsrc." + + def perform(type = nil, *plugin_args) + plugin_args << "--help" unless type == "new" + + unless options.key?("no_rc") # Thor's not so indifferent access hash. + railsrc = File.expand_path(options[:rc]) + + if File.exist?(railsrc) + extra_args = File.read(railsrc).split(/\n+/).flat_map(&:split) + puts "Using #{extra_args.join(" ")} from #{railsrc}" + plugin_args.insert(1, *extra_args) + end + end + + run_plugin_generator plugin_args + end + + private + def run_plugin_generator(plugin_args) + require "rails/generators" + require "rails/generators/rails/plugin/plugin_generator" + Rails::Generators::PluginGenerator.start plugin_args + end + end + end +end diff --git a/railties/lib/rails/commands/rake/rake_command.rb b/railties/lib/rails/commands/rake/rake_command.rb new file mode 100644 index 0000000000..a4a2464445 --- /dev/null +++ b/railties/lib/rails/commands/rake/rake_command.rb @@ -0,0 +1,49 @@ +module Rails + module Command + class RakeCommand < Base + namespace "rake" + + class << self + def printing_commands + formatted_rake_tasks.map(&:first) + end + + def perform(task, *) + require_rake + + ARGV.unshift(task) # Prepend the task, so Rake knows how to run it. + + Rake.application.standard_exception_handling do + Rake.application.init("rails") + Rake.application.load_rakefile + Rake.application.top_level + end + end + + private + def rake_tasks + require_rake + + return @rake_tasks if defined?(@rake_tasks) + + ActiveSupport::Deprecation.silence do + Rails::Command.require_application_and_environment! + end + + Rake::TaskManager.record_task_metadata = true + Rake.application.instance_variable_set(:@name, "rails") + Rails.application.load_tasks + @rake_tasks = Rake.application.tasks.select(&:comment) + end + + def formatted_rake_tasks + rake_tasks.map { |t| [ t.name_with_args, t.comment ] } + end + + def require_rake + require "rake" # Defer booting Rake until we know it's needed. + end + end + end + end +end diff --git a/railties/lib/rails/commands/runner/USAGE b/railties/lib/rails/commands/runner/USAGE new file mode 100644 index 0000000000..dc47a35ff3 --- /dev/null +++ b/railties/lib/rails/commands/runner/USAGE @@ -0,0 +1,17 @@ +Examples: + +Run `puts Rails.env` after loading the app: + + <%= executable %> 'puts Rails.env' + +Run the Ruby file located at `path/to/filename.rb` after loading the app: + + <%= executable %> path/to/filename.rb + +<% if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ %> +You can also use the runner command as a shebang line for your executables: + + #!/usr/bin/env <%= File.expand_path(executable) %> + + Product.all.each { |p| p.price *= 2 ; p.save! } +<% end %> diff --git a/railties/lib/rails/commands/runner/runner_command.rb b/railties/lib/rails/commands/runner/runner_command.rb new file mode 100644 index 0000000000..8db6da8759 --- /dev/null +++ b/railties/lib/rails/commands/runner/runner_command.rb @@ -0,0 +1,45 @@ +module Rails + module Command + class RunnerCommand < Base + class_option :environment, aliases: "-e", type: :string, + default: Rails::Command.environment.dup, + desc: "The environment for the runner to operate under (test/development/production)" + + def help + super + puts self.class.desc + end + + def self.banner(*) + "#{super} [<'Some.ruby(code)'> | ]" + end + + def perform(code_or_file = nil) + unless code_or_file + help + exit 1 + end + + ENV["RAILS_ENV"] = options[:environment] + + require_application_and_environment! + Rails.application.load_runner + + if File.exist?(code_or_file) + $0 = code_or_file + Kernel.load code_or_file + else + begin + eval(code_or_file, binding, __FILE__, __LINE__) + rescue SyntaxError, NameError => error + $stderr.puts "Please specify a valid ruby command or the path of a script to run." + $stderr.puts "Run '#{self.class.executable} -h' for help." + $stderr.puts + $stderr.puts error + exit 1 + end + end + end + end + end +end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb new file mode 100644 index 0000000000..b45805bddd --- /dev/null +++ b/railties/lib/rails/commands/server/server_command.rb @@ -0,0 +1,159 @@ +require "fileutils" +require "optparse" +require "action_dispatch" +require "rails" +require "rails/dev_caching" + +module Rails + class Server < ::Rack::Server + class Options + DEFAULT_PID_PATH = File.expand_path("tmp/pids/server.pid").freeze + + def parse!(args) + args, options = args.dup, {} + + option_parser(options).parse! args + + options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" + options[:server] = args.shift + options + end + + def option_parser(options) # :nodoc: + OptionParser.new do |opts| + opts.banner = "Usage: rails server [mongrel, thin etc] [options]" + + opts.separator "" + opts.separator "Options:" + + opts.on("-p", "--port=port", Integer, + "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v } + opts.on("-b", "--binding=IP", String, + "Binds Rails to the specified IP.", "Default: localhost") { |v| options[:Host] = v } + opts.on("-c", "--config=file", String, + "Uses a custom rackup configuration.") { |v| options[:config] = v } + opts.on("-d", "--daemon", "Runs server as a Daemon.") { options[:daemonize] = true } + opts.on("-e", "--environment=name", String, + "Specifies the environment to run this server under (test/development/production).", + "Default: development") { |v| options[:environment] = v } + opts.on("-P", "--pid=pid", String, + "Specifies the PID file.", + "Default: tmp/pids/server.pid") { |v| options[:pid] = v } + opts.on("-C", "--[no-]dev-caching", + "Specifies whether to perform caching in development.", + "true or false") { |v| options[:caching] = v } + + opts.separator "" + + opts.on("-h", "--help", "Shows this help message.") { puts opts; exit } + end + end + end + + def initialize(*) + super + set_environment + end + + # TODO: this is no longer required but we keep it for the moment to support older config.ru files. + def app + @app ||= begin + app = super + app.respond_to?(:to_app) ? app.to_app : app + end + end + + def opt_parser + Options.new + end + + def set_environment + ENV["RAILS_ENV"] ||= options[:environment] + end + + def start + print_boot_information + trap(:INT) { exit } + create_tmp_directories + setup_dev_caching + log_to_stdout if options[:log_stdout] + + super + ensure + # The '-h' option calls exit before @options is set. + # If we call 'options' with it unset, we get double help banners. + puts "Exiting" unless @options && options[:daemonize] + end + + def middleware + Hash.new([]) + end + + def default_options + super.merge( Port: ENV.fetch("PORT", 3000).to_i, + DoNotReverseLookup: true, + environment: (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup, + daemonize: false, + caching: nil, + pid: Options::DEFAULT_PID_PATH, + restart_cmd: restart_command) + end + + private + + def setup_dev_caching + if options[:environment] == "development" + Rails::DevCaching.enable_by_argument(options[:caching]) + end + end + + def print_boot_information + url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" + puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}" + puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}" + puts "=> Run `rails server -h` for more startup options" + end + + def create_tmp_directories + %w(cache pids sockets).each do |dir_to_make| + FileUtils.mkdir_p(File.join(Rails.root, "tmp", dir_to_make)) + end + end + + def log_to_stdout + wrapped_app # touch the app so the logger is set up + + console = ActiveSupport::Logger.new(STDOUT) + console.formatter = Rails.logger.formatter + console.level = Rails.logger.level + + unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT) + Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) + end + end + + def restart_command + "bin/rails server #{ARGV.join(' ')}" + end + end + + module Command + class ServerCommand < Base + def help # :nodoc: + puts Rails::Server::Options.new.option_parser(Hash.new) + end + + def perform + set_application_directory! + + Rails::Server.new.tap do |server| + # Require application after server sets environment to propagate + # the --environment option. + require APP_PATH + Dir.chdir(Rails.application.root) + server.start + end + end + end + end +end diff --git a/railties/lib/rails/commands/test/test_command.rb b/railties/lib/rails/commands/test/test_command.rb new file mode 100644 index 0000000000..395edca933 --- /dev/null +++ b/railties/lib/rails/commands/test/test_command.rb @@ -0,0 +1,18 @@ +require "rails/command" +require "rails/test_unit/minitest_plugin" + +module Rails + module Command + class TestCommand < Base + def help # :nodoc: + perform # Hand over help printing to minitest. + end + + def perform(*) + $LOAD_PATH << Rails::Command.root.join("test") + + exit Minitest.run(ARGV) + end + end + end +end diff --git a/railties/lib/rails/commands/version/version_command.rb b/railties/lib/rails/commands/version/version_command.rb new file mode 100644 index 0000000000..4f3fbfca1b --- /dev/null +++ b/railties/lib/rails/commands/version/version_command.rb @@ -0,0 +1,9 @@ +module Rails + module Command + class VersionCommand < Base + def perform + Rails::Command.invoke :application, [ "--version" ] + end + end + end +end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index fd35dfd1a2..dd16b44786 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -2,6 +2,7 @@ activesupport_path = File.expand_path("../../../../activesupport/lib", __FILE__) $:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) require "thor/group" +require "rails/command" require "active_support" require "active_support/core_ext/object/blank" @@ -13,6 +14,8 @@ require "active_support/core_ext/string/inflections" module Rails module Generators + include Rails::Command::Behavior + autoload :Actions, "rails/generators/actions" autoload :ActiveModel, "rails/generators/active_model" autoload :Base, "rails/generators/base" @@ -127,67 +130,6 @@ module Rails Thor::Base.shell = Thor::Shell::Basic end - # Track all generators subclasses. - def self.subclasses - @subclasses ||= [] - end - - # Rails finds namespaces similar to thor, it only adds one rule: - # - # Generators names must end with "_generator.rb". This is required because Rails - # looks in load paths and loads the generator just before it's going to be used. - # - # find_by_namespace :webrat, :rails, :integration - # - # Will search for the following generators: - # - # "rails:webrat", "webrat:integration", "webrat" - # - # Notice that "rails:generators:webrat" could be loaded as well, what - # Rails looks for is the first and last parts of the namespace. - def self.find_by_namespace(name, base=nil, context=nil) #:nodoc: - lookups = [] - lookups << "#{base}:#{name}" if base - lookups << "#{name}:#{context}" if context - - unless base || context - unless name.to_s.include?(?:) - lookups << "#{name}:#{name}" - lookups << "rails:#{name}" - end - lookups << "#{name}" - end - - lookup(lookups) - - namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }] - - lookups.each do |namespace| - klass = namespaces[namespace] - return klass if klass - end - - invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name) - end - - # Receives a namespace, arguments and the behavior to invoke the generator. - # It's used as the default entry point for generate, destroy and update - # commands. - def self.invoke(namespace, args=ARGV, config={}) - names = namespace.to_s.split(":") - if klass = find_by_namespace(names.pop, names.any? && names.join(":")) - args << "--help" if args.empty? && klass.arguments.any?(&:required?) - klass.start(args, config) - else - options = sorted_groups.flat_map(&:last) - suggestions = options.sort_by { |suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3) - msg = "Could not find generator '#{namespace}'. " - msg << "Maybe you meant #{ suggestions.map { |s| "'#{s}'" }.to_sentence(last_word_connector: " or ", locale: :en) }\n" - msg << "Run `rails generate --help` for more options." - puts msg - end - end - # Returns an array of generator namespaces that are hidden. # Generator namespaces may be hidden for a variety of reasons. # Some are aliased such as "rails:migration" and can be @@ -260,11 +202,13 @@ module Rails def self.sorted_groups namespaces = public_namespaces namespaces.sort! + groups = Hash.new { |h,k| h[k] = [] } namespaces.each do |namespace| base = namespace.split(":").first groups[base] << namespace end + rails = groups.delete("rails") rails.map! { |n| n.sub(/^rails:/, "") } rails.delete("app") @@ -272,64 +216,69 @@ module Rails hidden_namespaces.each { |n| groups.delete(n.to_s) } - [["rails", rails]] + groups.sort.to_a + [[ "rails", rails ]] + groups.sort.to_a + end + + # Rails finds namespaces similar to thor, it only adds one rule: + # + # Generators names must end with "_generator.rb". This is required because Rails + # looks in load paths and loads the generator just before it's going to be used. + # + # find_by_namespace :webrat, :rails, :integration + # + # Will search for the following generators: + # + # "rails:webrat", "webrat:integration", "webrat" + # + # Notice that "rails:generators:webrat" could be loaded as well, what + # Rails looks for is the first and last parts of the namespace. + def self.find_by_namespace(name, base = nil, context = nil) #:nodoc: + lookups = [] + lookups << "#{base}:#{name}" if base + lookups << "#{name}:#{context}" if context + + unless base || context + unless name.to_s.include?(?:) + lookups << "#{name}:#{name}" + lookups << "rails:#{name}" + end + lookups << "#{name}" + end + + lookup(lookups) + + namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }] + lookups.each do |namespace| + + klass = namespaces[namespace] + return klass if klass + end + + invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name) + end + + # Receives a namespace, arguments and the behavior to invoke the generator. + # It's used as the default entry point for generate, destroy and update + # commands. + def self.invoke(namespace, args=ARGV, config={}) + names = namespace.to_s.split(":") + if klass = find_by_namespace(names.pop, names.any? && names.join(":")) + args << "--help" if args.empty? && klass.arguments.any?(&:required?) + klass.start(args, config) + else + options = sorted_groups.flat_map(&:last) + suggestions = options.sort_by { |suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3) + msg = "Could not find generator '#{namespace}'. " + msg << "Maybe you meant #{ suggestions.map { |s| "'#{s}'" }.to_sentence(last_word_connector: " or ", locale: :en) }\n" + msg << "Run `rails generate --help` for more options." + puts msg + end end protected - - # This code is based directly on the Text gem implementation. - # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher. - # - # Returns a value representing the "cost" of transforming str1 into str2 - def self.levenshtein_distance(str1, str2) - s = str1 - t = str2 - n = s.length - m = t.length - - return m if (0 == n) - return n if (0 == m) - - d = (0..m).to_a - x = nil - - # avoid duplicating an enumerable object in the loop - str2_codepoint_enumerable = str2.each_codepoint - - str1.each_codepoint.with_index do |char1, i| - e = i+1 - - str2_codepoint_enumerable.with_index do |char2, j| - cost = (char1 == char2) ? 0 : 1 - x = [ - d[j+1] + 1, # insertion - e + 1, # deletion - d[j] + cost # substitution - ].min - d[j] = e - e = x - end - - d[m] = x - end - - x - end - - # Prints a list of generators. - def self.print_list(base, namespaces) #:nodoc: - namespaces = namespaces.reject do |n| - hidden_namespaces.include?(n) - end - - return if namespaces.empty? - puts "#{base.camelize}:" - - namespaces.each do |namespace| - puts(" #{namespace}") - end - - puts + def self.print_list(base, namespaces) + namespaces = namespaces.reject { |n| hidden_namespaces.include?(n) } + super end # Try fallbacks for the given base. @@ -348,53 +297,16 @@ module Rails nil end - # Receives namespaces in an array and tries to find matching generators - # in the load path. - def self.lookup(namespaces) #:nodoc: - paths = namespaces_to_paths(namespaces) - - paths.each do |raw_path| - ["rails/generators", "generators"].each do |base| - path = "#{base}/#{raw_path}_generator" - - begin - require path - return - rescue LoadError => e - raise unless e.message =~ /#{Regexp.escape(path)}$/ - rescue Exception => e - warn "[WARNING] Could not load generator #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}" - end - end - end + def self.command_type + @command_type ||= "generator" end - # This will try to load any generator in the load path to show in help. - def self.lookup! #:nodoc: - $LOAD_PATH.each do |base| - Dir[File.join(base, "{rails/generators,generators}", "**", "*_generator.rb")].each do |path| - begin - path = path.sub("#{base}/", "") - require path - rescue Exception - # No problem - end - end - end + def self.lookup_paths + @lookup_paths ||= %w( rails/generators generators ) end - # Convert namespaces to paths by replacing ":" for "/" and adding - # an extra lookup. For example, "rails:model" should be searched - # in both: "rails/model/model_generator" and "rails/model_generator". - def self.namespaces_to_paths(namespaces) #:nodoc: - paths = [] - namespaces.each do |namespace| - pieces = namespace.split(":") - paths << pieces.dup.push(pieces.last).join("/") - paths << pieces.join("/") - end - paths.uniq! - paths + def self.file_lookup_paths + @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_generator.rb" ] end end end diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb index 6e196a32ab..e294f2de83 100644 --- a/railties/lib/rails/test_unit/minitest_plugin.rb +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -1,4 +1,5 @@ require "active_support/core_ext/module/attribute_accessors" +require "active_support/core_ext/hash/keys" require "rails/test_unit/reporter" require "rails/test_unit/test_requirer" require "shellwords" diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb index 4dc766dfda..77e7a2cca5 100644 --- a/railties/test/application/runner_test.rb +++ b/railties/test/application/runner_test.rb @@ -82,7 +82,7 @@ module ApplicationTests def test_runner_detects_bad_script_name output = Dir.chdir(app_path) { `bin/rails runner "iuiqwiourowe" 2>&1` } assert_not $?.success? - assert_match "undefined local variable or method `iuiqwiourowe' for main:Object", output + assert_match "undefined local variable or method `iuiqwiourowe' for", output end def test_environment_with_rails_env