├── .gitignore ├── ChangeLog.md ├── README.md ├── bin └── effuse ├── effuse.gemspec └── lib └── effuse.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 2.1.0 (November 10 2013) 4 | 5 | * Added support for `effuse.yml` files 6 | * Deprecated `.effuseignore` files 7 | * Added `--version` option 8 | 9 | ## 2.0.1 (May 14 2013) 10 | 11 | * Fix: Backup files by appending `.effuse` 12 | * Fix: Ignore effuse backup files 13 | 14 | ## 2.0.0 (May 13 2013) 15 | 16 | * Replace existing files by default 17 | * Added `--import` option to import existing files 18 | * Changed `--noconfirm` to `--no-confirm` 19 | * Backup files to `.file.effuse` 20 | * Removed short forms of `--exclude` and `--include` options 21 | 22 | ## 1.1.1 (March 12 2013) 23 | 24 | * Dropped Ruby 1.8 compatibility 25 | 26 | ## 1.1.0 (April 28 2012) 27 | 28 | * Added `--prefix` 29 | * Fix: Ruby 1.8 compatibility in reading `.effuseignore` 30 | 31 | ## 1.0.0 (April 9 2012) 32 | 33 | * Added `--no-backup` 34 | * Support for `.effuseignore` 35 | 36 | ## 0.3.2 (March 31 2012) 37 | 38 | * Fix: Exclude `.*~` files from symlinking 39 | 40 | ## 0.3.1 (March 29 2012) 41 | 42 | * Fix: Check if destination directory is current directory 43 | 44 | ## 0.3.0 (January 23 2012) 45 | 46 | * Added `--noconfirm` option 47 | 48 | * Fix: Ruby 1.8 compatibility 49 | 50 | ## 0.2.0 (January 22 2012) 51 | 52 | * Added `--include` option 53 | 54 | * Fix: Ignore `.git`, `.gitignore`, `.gitmodules` instead of `.git*` 55 | 56 | ## 0.1.2 (January 21 2012) 57 | 58 | * Fix: Ruby 1.8 compatibility 59 | 60 | ## 0.1.1 (January 21 2012) 61 | 62 | * Fix: Ruby 1.8 compatibility 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Effuse 2 | 3 | Tool for symlinking dotfiles. 4 | 5 | ## Install 6 | 7 | ``` 8 | gem install effuse 9 | ``` 10 | 11 | ## Usage 12 | 13 | So, say you have all your precious dotfiles in a nice little git 14 | repository or Dropbox folder or whatever else you might use. It's a 15 | great way to be able to use your configurations on multiple computers, 16 | but how do you get those configurations out of the repository and into 17 | your filesystem? 18 | 19 | The answer: symbolic links. Lots of them. And how do you go about 20 | creating all these symlinks? Sure, you could create each one by hand, 21 | but you're a busy person and obviously don't have time for such menial 22 | tasks. Luckily, you've just discovered Effuse! 23 | 24 | Just run `effuse` in your dotfiles repository and it will automagically 25 | create symlinks to all your configurations in your home directory! 26 | 27 | ```sh 28 | dotfiles $ ls -a 29 | . .. .vimrc .zshrc 30 | dotfiles $ effuse 31 | '/home/you/.vimrc' -> '/home/you/dotfiles/.vimrc' 32 | '/home/you/.zshrc' -> '/home/you/dotfiles/.zshrc' 33 | ``` 34 | 35 | Your configurations don't go in your home directory? No problem! Just 36 | run `effuse /path/` and the symlinks will be created wherever your heart 37 | desires. 38 | 39 | Would you rather import a dotfile from your system into your repository 40 | than replace it? No problem, just use `effuse --import`. 41 | 42 | ```sh 43 | dotfiles $ ls -a ~ 44 | . .. .vimrc .zshrc 45 | dotfiles $ touch .vimrc .zshrc 46 | dotfiles $ effuse --import 47 | '/home/you/.vimrc' already exists. Import it? [Y/n] y 48 | '/home/you/.vimrc' -> '/home/you/dotfiles/.vimrc' 49 | '/home/you/.zshrc' already exists. Import it? [Y/n] y 50 | '/home/you/.zshrc' -> '/home/you/dotfiles/.zshrc' 51 | ``` 52 | 53 | Symlinks aren't working out for you? Effuse has got you covered. Just 54 | run `effuse --clean` in your dotfiles repository and it will remove all 55 | those nasty symlinks it created before. 56 | 57 | Don't want to symlink a certain bothersome file? Why not tell Effuse 58 | your opinion on the matter using `effuse --exclude file`? 59 | 60 | ```sh 61 | dotfiles $ ls -a 62 | . .. .vimrc .zshrc 63 | dotfiles $ effuse --exclude .vimrc 64 | '/home/you/.zshrc' -> '/home/you/dotfiles/.zshrc' 65 | ``` 66 | 67 | Maybe you don't like to have the files in your dotfiles repository named 68 | with leading dots. If that's what floats your boat, you can have Effuse 69 | prefix the symlink paths using `effuse --prefix .`. 70 | 71 | ```sh 72 | dotfiles $ ls 73 | vimrc zshrc 74 | dotfiles $ effuse --prefix . 75 | '/home/you/.vimrc' -> '/home/you/dotfiles/vimrc' 76 | '/home/you/.zshrc' -> '/home/you/dotfiles/zshrc' 77 | ``` 78 | 79 | Now, say you want to create symlinks in `~/foo`, you want to exclude 80 | `*.bak` files, and you want to prefix your symlink paths with `.`. 81 | Specifying all those on the command line every time you run Effuse 82 | sounds horrible, doesn't it? Luckily, you can use an `effuse.yml` file 83 | instead! 84 | 85 | ```yaml 86 | destination: ~/foo 87 | prefix: . 88 | exclude: 89 | - '*.bak' 90 | ``` 91 | 92 | ### Command Line 93 | 94 | ``` 95 | usage: effuse [OPTIONS...] [DEST] 96 | -i, --import Import existing files 97 | -c, --clean Remove symlinks 98 | 99 | -y, --no-confirm Do not ask before replacing files 100 | -n, --no-backup Do not create backup files 101 | 102 | --exclude GLOB Exclude GLOB from symlinking 103 | --include GLOB Include GLOB in symlinking 104 | 105 | -p, --prefix Prefix symlink paths with PREFIX 106 | 107 | -v, --verbose Show verbose output 108 | 109 | --version Show version 110 | -h, --help Show this message 111 | ``` 112 | 113 | ### YAML File 114 | 115 | The `effuse.yml` file may contain the following keys: 116 | 117 | * `destination`: Symlink destination directory (defaults to `~`) 118 | * `prefix`: Symlink path prefix 119 | * `exclude`: Array of globs to exclude from symlinking 120 | * `include`: Array of globs to not exclude from symlinking 121 | 122 | ### Migrating from 2.0 to 2.1 123 | 124 | Version 2.1 of Effuse deprecates the use of `.effuseignore` files in 125 | favor of `effuse.yml` files. To migrate, add each line of the ignore 126 | file to the `exclude` array of the YAML file. 127 | 128 | `.effuseignore`: 129 | 130 | ``` 131 | *.bak 132 | foo 133 | ``` 134 | 135 | `effuse.yml`: 136 | 137 | ```yaml 138 | exclude: 139 | - '*.bak' 140 | - 'foo' 141 | ``` 142 | 143 | ## License 144 | 145 | Copyright © 2012-2013, Curtis McEnroe 146 | 147 | Permission to use, copy, modify, and/or distribute this software for any 148 | purpose with or without fee is hereby granted, provided that the above 149 | copyright notice and this permission notice appear in all copies. 150 | 151 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 152 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 153 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 154 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 155 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 156 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 157 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 158 | -------------------------------------------------------------------------------- /bin/effuse: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'effuse' 3 | Effuse.new.execute 4 | -------------------------------------------------------------------------------- /effuse.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.expand_path('../lib', __FILE__) 2 | require 'effuse' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'effuse' 6 | s.version = Effuse::VERSION 7 | s.authors = ['Curtis McEnroe'] 8 | s.email = ['programble@gmail.com'] 9 | s.homepage = 'https://github.com/programble/effuse' 10 | s.licenses = ['ISC'] 11 | s.summary = 'Tool for symlinking dotfiles' 12 | s.description = s.summary 13 | 14 | s.files = `git ls-files`.split("\n") 15 | s.executables = 'effuse' 16 | s.require_paths = ['lib'] 17 | end 18 | -------------------------------------------------------------------------------- /lib/effuse.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'yaml' 3 | require 'optparse' 4 | 5 | class Effuse 6 | VERSION = '2.1.0' 7 | 8 | def initialize 9 | @dest_dir = Dir.home 10 | @import = false 11 | @clean = false 12 | @confirm = true 13 | @backup = true 14 | @prefix = '' 15 | @verbose = false 16 | @exclude = %w[*~ .*~ .*.sw? .effuseignore effuse.yml *.effuse .*.effuse 17 | .git .gitignore .gitmodules] 18 | 19 | ignore_file 20 | effuse_file 21 | options 22 | 23 | vputs self.inspect 24 | end 25 | 26 | def ignore_file 27 | return unless File.file? '.effuseignore' 28 | puts 'warning: ignore file is deprecated, use effuse.yml instead' 29 | File.readlines('.effuseignore').each do |glob| 30 | @exclude << glob.chomp 31 | end 32 | end 33 | 34 | def effuse_file 35 | return unless File.file? 'effuse.yml' 36 | yaml = YAML.load_file('effuse.yml') 37 | 38 | if yaml.include? 'destination' 39 | @dest_dir = File.expand_path(yaml['destination']) 40 | end 41 | @prefix = yaml['prefix'] if yaml.include? 'prefix' 42 | 43 | exclude = yaml['exclude'] || yaml['ignore'] 44 | @exclude.concat(exclude) if exclude 45 | @exclude -= yaml['include'] if yaml.include? 'include' 46 | end 47 | 48 | def options 49 | OptionParser.new do |o| 50 | o.banner = 'usage: effuse [OPTIONS...] [DEST]' 51 | 52 | o.on('-i', '--import', 'Import existing files') do 53 | @import = true 54 | end 55 | o.on('-c', '--clean', 'Remove symlinks') do 56 | @clean = true 57 | end 58 | 59 | o.separator '' 60 | 61 | o.on('-y', '--no-confirm', 'Do not ask before replacing files') do 62 | @confirm = false 63 | end 64 | o.on('-n', '--no-backup', 'Do not create backup files') do 65 | @backup = false 66 | end 67 | 68 | o.separator '' 69 | 70 | o.on('--exclude GLOB', 'Exclude GLOB from symlinking') do |glob| 71 | @exclude << glob 72 | end 73 | o.on('--include GLOB', 'Include GLOB in symlinking') do |glob| 74 | @exclude.delete(glob) 75 | end 76 | 77 | o.separator '' 78 | 79 | o.on('-p', '--prefix', 'Prefix symlink paths with PREFIX') do |prefix| 80 | @prefix = prefix 81 | end 82 | 83 | o.separator '' 84 | 85 | o.on('-v', '--verbose', 'Show verbose output') do 86 | @verbose = true 87 | end 88 | 89 | o.separator '' 90 | 91 | o.on_tail('--version', 'Show version') do 92 | puts "effuse #{VERSION}" 93 | exit 94 | end 95 | o.on_tail('-h', '--help', 'Show this message') do 96 | puts o 97 | exit 98 | end 99 | end.parse! 100 | @dest_dir = ARGV.first if ARGV.first 101 | end 102 | 103 | def vputs(s) 104 | puts(s) if @verbose 105 | end 106 | 107 | def confirm? s 108 | return true unless @confirm 109 | loop do 110 | print "#{s} [Y/n] " 111 | input = $stdin.gets.chomp 112 | return true if input.empty? || input[0].downcase == ?y 113 | return false if input[0].downcase == ?n 114 | end 115 | end 116 | 117 | def backup(src) 118 | if @backup 119 | vputs "renaming '#{src}' -> '#{src}.effuse'" 120 | File.rename(src, src + '.effuse') 121 | else 122 | vputs "deleting '#{src}'" 123 | File.delete(src) 124 | end 125 | end 126 | 127 | def scan 128 | dirs = ['.'] 129 | files = {} 130 | 131 | dirs.each do |dir| 132 | vputs "scanning '#{dir}'" 133 | Dir.foreach(dir) do |entry| 134 | next if %w[. ..].include? entry 135 | 136 | path = File.join(dir, entry) 137 | 138 | if @exclude.any? {|g| File.fnmatch(g, entry) } 139 | vputs "excluding '#{path}'" 140 | next 141 | end 142 | 143 | if File.directory? path 144 | dirs << path 145 | next 146 | end 147 | 148 | vputs "adding '#{path}'" 149 | relpath = @prefix + File.join(path.split('/').drop(1)) 150 | files[File.absolute_path(path)] = File.join(@dest_dir, relpath) 151 | end 152 | end 153 | 154 | files 155 | end 156 | 157 | def clean(files) 158 | files.each do |src, dest| 159 | if File.exist?(dest) && File.identical?(src, dest) 160 | puts dest 161 | File.delete(dest) 162 | else 163 | vputs "'#{dest}' not symlinked" 164 | end 165 | end 166 | end 167 | 168 | def symlink(files) 169 | files.each do |src, dest| 170 | if File.exist? dest 171 | if File.identical? src, dest 172 | vputs "'#{dest}' already symlinked" 173 | next 174 | elsif @import 175 | if confirm? "'#{dest}' already exists. Import it?" 176 | backup(src) 177 | vputs "renaming '#{dest}' -> '#{src}'" 178 | File.rename(dest, src) 179 | else 180 | vputs "skipping '#{dest}'" 181 | next 182 | end 183 | else # Replace destination file 184 | if confirm? "'#{dest}' already exists. Replace it?" 185 | backup(dest) 186 | else 187 | vputs "skipping '#{dest}'" 188 | next 189 | end 190 | end 191 | end 192 | 193 | puts "'#{dest}' -> '#{src}'" 194 | FileUtils.mkdir_p(File.dirname(dest)) 195 | File.symlink(src, dest) 196 | end 197 | end 198 | 199 | def execute 200 | if File.identical? @dest_dir, '.' 201 | puts 'error: destination directory is current directory' 202 | exit 1 203 | end 204 | 205 | if @clean 206 | clean(scan) 207 | else 208 | symlink(scan) 209 | end 210 | end 211 | end 212 | --------------------------------------------------------------------------------