2015-07-23 16:38:01 +00:00
|
|
|
#!/usr/bin/env ruby
|
|
|
|
|
2018-06-27 13:52:45 +00:00
|
|
|
require 'optparse'
|
2015-07-23 16:38:01 +00:00
|
|
|
require 'pathname'
|
|
|
|
require 'fileutils'
|
|
|
|
|
|
|
|
module ShellHelpers
|
|
|
|
extend self
|
|
|
|
|
|
|
|
COLOR_CODES = {
|
|
|
|
black: 0, light_black: 60,
|
|
|
|
red: 1, light_red: 61,
|
|
|
|
green: 2, light_green: 62,
|
|
|
|
yellow: 3, light_yellow: 63,
|
|
|
|
blue: 4, light_blue: 64,
|
|
|
|
magenta: 5, light_magenta: 65,
|
|
|
|
cyan: 6, light_cyan: 66,
|
|
|
|
white: 7, light_white: 67,
|
|
|
|
default: 9
|
|
|
|
}.freeze
|
|
|
|
|
|
|
|
def colorify_string(str, color = :default)
|
|
|
|
"\033[0;#{COLOR_CODES[color] + 30}m#{str}\033[0m"
|
|
|
|
end
|
|
|
|
|
|
|
|
def say(message, color = :default)
|
|
|
|
$stdout.print(colorify_string(message, color))
|
|
|
|
$stdout.flush
|
|
|
|
end
|
|
|
|
|
|
|
|
def say_status(status, message, color = :default)
|
|
|
|
buffer = "#{colorify_string(status, color)} #{message}"
|
|
|
|
buffer << "\n" unless buffer.end_with?("\n")
|
|
|
|
|
|
|
|
$stdout.print(buffer)
|
|
|
|
$stdout.flush
|
|
|
|
end
|
|
|
|
|
|
|
|
def ask_simply(statement, color = :default)
|
|
|
|
say("#{statement} ", color)
|
|
|
|
|
|
|
|
$stdin.gets.tap { |text| text.strip! if text }
|
|
|
|
end
|
|
|
|
|
|
|
|
def ask_filtered(statement, answer_set, color = :default)
|
|
|
|
correct_answer = nil
|
|
|
|
until correct_answer
|
|
|
|
answer = ask_simply("#{statement} [#{answer_set.join}]", color)
|
|
|
|
correct_answer = answer_set.include?(answer) ? answer : nil
|
|
|
|
|
|
|
|
answers = answer_set.join
|
|
|
|
say("Your response must be one of: [#{answers}]. Please try again.") unless correct_answer
|
|
|
|
end
|
|
|
|
|
|
|
|
correct_answer
|
|
|
|
end
|
|
|
|
|
|
|
|
def ask(statement, *args)
|
|
|
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
|
|
color = args.first
|
|
|
|
|
|
|
|
if options[:limited_to]
|
|
|
|
ask_filtered(statement, options[:limited_to], color)
|
|
|
|
else
|
|
|
|
ask_simply(statement, color)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def collision_accepted?(destination)
|
|
|
|
answer = ask("Overwrite #{destination}?", :default, limited_to: %w(y n a))
|
|
|
|
|
|
|
|
case answer
|
|
|
|
when 'y'
|
|
|
|
return true
|
|
|
|
when 'n'
|
|
|
|
return false
|
|
|
|
when 'a'
|
|
|
|
say 'Aborting...'
|
|
|
|
fail SystemExit
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
module FileHelpers
|
|
|
|
extend self
|
|
|
|
|
|
|
|
def inside(dir, &block)
|
|
|
|
dir = Pathname.new(dir).expand_path
|
|
|
|
|
|
|
|
FileUtils.mkdir_p(dir) unless File.exist?(dir)
|
|
|
|
FileUtils.cd(dir) { block.arity == 1 ? yield(dir) : yield }
|
|
|
|
end
|
|
|
|
|
|
|
|
def descendant?(a, b)
|
|
|
|
a_list = Pathname.new(a).expand_path.to_s.split('/')
|
|
|
|
b_list = Pathname.new(b).expand_path.to_s.split('/')
|
|
|
|
|
|
|
|
b_list[0..a_list.size - 1] == a_list
|
|
|
|
end
|
|
|
|
|
|
|
|
def rm_link(target)
|
|
|
|
target = Pathname.new(target)
|
|
|
|
|
|
|
|
if target.symlink?
|
|
|
|
say_status(:unlink, "#{target.expand_path}", :green)
|
|
|
|
FileUtils.rm_rf(target)
|
|
|
|
else
|
|
|
|
say_status(:conflict, "#{target} is not a symlink", :red)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def ln_s(source, destination, options = {})
|
|
|
|
source = Pathname.new(source)
|
|
|
|
destination = Pathname.new(destination)
|
|
|
|
|
|
|
|
FileUtils.mkdir_p(destination.dirname)
|
|
|
|
|
|
|
|
if destination.symlink? && destination.readlink == source
|
|
|
|
say_status(:identical, destination.expand_path, :blue)
|
|
|
|
elsif destination.symlink?
|
|
|
|
say_status(:conflict, "#{destination} exists and points to #{destination.readlink}", :red)
|
|
|
|
|
|
|
|
FileUtils.rm(destination)
|
|
|
|
FileUtils.ln_s(source, destination, force: true)
|
|
|
|
elsif destination.exist?
|
|
|
|
say_status(:conflict, "#{destination} exists", :red)
|
|
|
|
|
2018-06-27 13:52:45 +00:00
|
|
|
if options[:force] || collision_accepted?(destination)
|
2015-07-23 16:38:01 +00:00
|
|
|
FileUtils.rm_r(destination, force: true)
|
|
|
|
FileUtils.ln_s(source, destination, force: true)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
say_status(:symlink, "#{source.expand_path} to #{destination.expand_path}", :green)
|
|
|
|
|
|
|
|
FileUtils.ln_s(source, destination)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Linker
|
|
|
|
include FileHelpers
|
|
|
|
include ShellHelpers
|
|
|
|
|
2018-06-27 13:52:45 +00:00
|
|
|
VERSION = '0.2.1'.freeze
|
2015-07-23 16:38:01 +00:00
|
|
|
LINKDIR_FILENAME = '.linkdir'.freeze
|
|
|
|
|
2018-06-27 13:52:45 +00:00
|
|
|
attr_reader :options
|
|
|
|
|
|
|
|
def initialize(options = {})
|
|
|
|
@options = options
|
2015-07-23 16:38:01 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def home_dir
|
|
|
|
@home_dir ||= Pathname.new(ENV['HOME'] || '~').realpath
|
|
|
|
end
|
|
|
|
|
|
|
|
def repo_dir
|
|
|
|
@repo_dir ||= Pathname.new(File.dirname(__FILE__)).join('home').realpath
|
|
|
|
end
|
|
|
|
|
|
|
|
def each_file
|
|
|
|
skip_dirs = []
|
|
|
|
|
|
|
|
inside(repo_dir) do
|
|
|
|
Pathname.glob('**/*', File::FNM_DOTMATCH).reject do |file|
|
|
|
|
['.', '..', LINKDIR_FILENAME].include?(file.basename.to_s)
|
|
|
|
end.each do |path|
|
|
|
|
if path.directory? && path.join(LINKDIR_FILENAME).exist?
|
|
|
|
skip_dirs << path
|
|
|
|
elsif path.directory? || skip_dirs.any? { |dir| descendant?(dir, path) }
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
yield(path.expand_path, home_dir.join(path))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def list_all
|
|
|
|
each_file do |absolute_path, home_path|
|
|
|
|
say_status(absolute_path.file? ? :file : :directory, home_path, :blue)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def symlink_all
|
|
|
|
each_file do |absolute_path, home_path|
|
2018-06-27 13:52:45 +00:00
|
|
|
ln_s(absolute_path, home_path, options)
|
2015-07-23 16:38:01 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def unsymlink_all
|
|
|
|
each_file do |_, home_path|
|
|
|
|
rm_link(home_path)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-06-27 13:52:45 +00:00
|
|
|
COMMANDS = ['list', 'update', 'link', 'unlink'].freeze
|
|
|
|
|
|
|
|
options = {}
|
|
|
|
opt_parser = OptionParser.new do |opts|
|
|
|
|
opts.banner = "Usage: #{__FILE__} <command> [options]"
|
|
|
|
|
|
|
|
opts.separator ''
|
|
|
|
opts.separator 'Commands:'
|
|
|
|
opts.separator 'list List files to link'
|
|
|
|
opts.separator 'link Symlink all files'
|
|
|
|
opts.separator 'unlink Unsymlink all files'
|
|
|
|
|
|
|
|
opts.separator ''
|
|
|
|
opts.separator 'Options:'
|
|
|
|
opts.on('-f', '--force', 'Force overwrite all files') { |force| options[:force] = force }
|
|
|
|
opts.on('-v', '--version', 'Show version information') { |_| ShellHelpers.say(Linker::VERSION); exit(0) }
|
|
|
|
end
|
2015-07-23 16:38:01 +00:00
|
|
|
|
2018-06-27 13:52:45 +00:00
|
|
|
opt_parser.parse!
|
2015-07-28 10:11:42 +00:00
|
|
|
command = ARGV.pop
|
|
|
|
if command.nil? || !COMMANDS.include?(command)
|
2018-06-27 13:52:45 +00:00
|
|
|
ShellHelpers.say(opt_parser.help)
|
2015-07-28 10:11:42 +00:00
|
|
|
else
|
2018-06-27 13:52:45 +00:00
|
|
|
linker = Linker.new(options)
|
2015-07-23 16:38:01 +00:00
|
|
|
|
2015-07-28 10:11:42 +00:00
|
|
|
case command
|
|
|
|
when 'list'
|
|
|
|
linker.list_all
|
2018-06-27 13:52:45 +00:00
|
|
|
when 'link'
|
2015-07-23 16:38:01 +00:00
|
|
|
linker.symlink_all
|
2018-06-27 13:52:45 +00:00
|
|
|
when 'unlink'
|
2015-07-23 16:38:01 +00:00
|
|
|
linker.unsymlink_all
|
|
|
|
end
|
|
|
|
end
|