Skip to content

Instantly share code, notes, and snippets.

@aks
Created December 5, 2023 20:20
Show Gist options
  • Save aks/72bd2d28a2dcc0cb386a099ce0b13f0b to your computer and use it in GitHub Desktop.
Save aks/72bd2d28a2dcc0cb386a099ce0b13f0b to your computer and use it in GitHub Desktop.
Ruby script to produce the spec files corresponding to the ruby file arguments.
#!/usr/bin/env ruby
# specs-for file ...
#
# Don't you just hate having to find the corresponding spec file for some files
# you just updated? And, you're not using Guard (for reasons)?
#
# pass all the files you touches as arguments into this script, and it will
# issue all the corresponding spec files.
#
# If you have "ag" installed, it will be used to find the files, otherwise, the
# Ruby Dir class is used (slightly slower than "ag").
#
# Free to use, but don't pretend you wrote it.
# Copyright Alan K. Stebbens <[email protected]>
PROG = File.basename($0)
require 'optparse'
require 'pry-byebug'
# This class find Specs for given files.
# rubocop:disable Metrics/ClassLength
class SpecsFor
def usage
warn <<~USAGE
usage: $PROG [-hev] FILE ..
Print the specs corresponding to file, if any. FILE may include partial
or complete paths to a ruby file.
Spec files are simply passed through, while ruby code files are converted
into spec paths by converting "app" or "lib" directories into "spec",
with corresponding subdirectories.
Uses "ag" to find the spec files, if "ag" is installed. Otherwise, uses
the ruby Dir class.
Options
-d debug mode
-h show this help
-e match FILE exactly
-v be verbose
USAGE
exit
end
# if FILE has no path at all,
# 1. find it in the current directory
# 2. find it across the project
# 3. report the corresponding spec file, if any
attr_reader :opts, :file_args, :files, :specs
def exact?; opts[:exact]; end
def norun?; opts[:norun]; end
def verbose?; opts[:verbose]; end
def debug?; opts[:debug]; end
def parse_options
@opts = {}
OptionParser.new do |opt|
opt.banner = "Usage: #{PROG} [options] FILE ..."
opt.on('-h', '--help') do
warn opt
exit
end
opt.on('-d', '--debug')
opt.on('-n', '--norun')
opt.on('-e', '--exact')
opt.on('-v', '--verbose')
end.parse!(into: @opts)
@file_args = ARGV
end
def find_files
file_args.each do |file_arg|
if File.exist?(file_arg)
@files << file_arg
else
found = find_file(file_arg)
if found.empty?
warn "==> Could not find: #{file_arg}"
else
show_files(found, 'Found file') if verbose?
@files.append(*found)
end
end
end
end
def find_file(file)
if ag_present?
find_file_with_ag(file)
else
find_file_with_dir(file)
end
end
def ag_present?
@ag_present ||= !`which ag`.empty?
end
def find_file_with_ag(file)
dir = File.dirname(file)
base = File.basename(file)
ext = File.extname(base)
plain = file.count('/').zero?
# if the file is plain, search for it recursively
# if the file has a directory, with exact, do not glob the dir path
# if exact matching is on, do not glob the filename
# if there is no extension, find any .rb extension
file_pat =
if plain && exact?
base
elsif plain
".*#{base}"
elsif exact?
"#{dir}/#{base}"
else
".*#{dir}/.*#{base}"
end
binding.pry if debug?
file_pat += exact? ? '\.rb$' : '*\.rb$' unless ext
`ag -S -g "#{file_pat}"`.split("\n")
end
def find_file_with_dir(file)
dir = File.dirname(file)
base = File.basename(file)
ext = File.extname(base)
plain = file.count('/').zero?
# if the file is plain, search for it recursively
# if the file has a directory, with exact, do not glob the dir path
# if exact matching is on, do not glob the filename
# if there is no extension, find any .rb extension
binding.pry if debug?
file_pat =
if plain && exact?
"**/#{base}"
elsif plain
"**/*#{base}"
elsif exact?
"#{dir}/#{base}"
else
"**/#{dir}/*#{base}"
end
file_pat += exact? ? '.rb' : '*.rb' unless ext
Dir[file_pat]
end
def show_files(files, what = nil)
if files.size == 1
warn "#{what} #{files.first}"
else
warn "#{what}s:"
warn ' ' + files.sort.join("\n ") # rubocop: disable Style/StringConcatenation
end
end
# look for app/**/*.rb or lib/**/*.rb and convert into spec/**/*_spec.rb
def find_specs
files.each do |file|
if file.end_with?('_spec.rb')
@specs << file
elsif file.match?(%r{(?:^|/)(?<dir>app|lib)/})
find_spec_file(file)
elsif verbose?
warn "I don't know how to find the spec for #{file}"
end
end
end
def find_spec_file(file)
spec_file = file.sub(%r{(?<=/|^)(lib|app)/}, 'spec/').sub(/\.rb$/, '_spec.rb')
if File.exist?(spec_file)
@specs << spec_file
warn "==> Found spec for #{file}" if verbose?
elsif verbose?
warn "==> No spec file for #{file}"
end
end
def show_specs
puts specs.join("\n")
end
def run
parse_options
find_files
find_specs
show_specs
end
def initialize
@file_args = []
@files = []
@specs = []
end
end
# rubocop:enable Metrics/ClassLength
SpecsFor.new.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment