├── .gitignore ├── .rspec ├── .semaphore └── semaphore.yml ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── bin ├── annotate_sgf ├── colcut ├── convert_to_png ├── countdown ├── dedup_files ├── diffschemas ├── e ├── fix_permissions ├── flickr_find ├── flickr_get ├── git_hash ├── gzip_stream ├── json_pp ├── kindle_sync ├── lastfm_status ├── media_size ├── namenorm ├── open_chrome ├── open_incognito ├── open_youtube ├── openmany ├── osx_screensaver ├── osx_suspend ├── pomodoro ├── process_gplus_takeout ├── progress ├── pub ├── rand_passwd ├── randsample ├── randswap ├── rbexe ├── rename ├── rjq ├── rot13 ├── sortby ├── speedup_mp3 ├── split_dir ├── sqlite2json ├── swap ├── tac ├── terminal_title ├── tfl_travel_time ├── toutf8 ├── trash_size ├── unall ├── volume ├── webman ├── xmlview ├── xpstree └── xrmdir └── spec ├── colcut_spec.rb ├── countdown_spec.rb ├── coverage_spec.rb ├── fix_permissions_spec.rb ├── flickr_find_spec.rb ├── gzip_stream_spec.rb ├── json_pp_spec.rb ├── lastfm_status_spec.rb ├── namenorm_spec.rb ├── open_youtube_spec.rb ├── openmany_spec.rb ├── rand_passwd_spec.rb ├── randsample_spec.rb ├── randswap_spec.rb ├── rbexe_spec.rb ├── rename_spec.rb ├── rjq_spec.rb ├── rot13_spec.rb ├── sortby_spec.rb ├── spec_helper.rb ├── split_dir_spec.rb ├── swap_spec.rb ├── tac_spec.rb ├── terminal_title_spec.rb ├── tfl_travel_time_spec.rb ├── toutf8 ├── ascii.txt ├── empty.txt ├── utf16be.txt ├── utf16be_bom.txt ├── utf16le.txt ├── utf16le_bom.txt ├── utf8.txt └── utf8_bom.txt ├── toutf8_spec.rb ├── unall_spec.rb └── xrmdir_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | 4 | -------------------------------------------------------------------------------- /.semaphore/semaphore.yml: -------------------------------------------------------------------------------- 1 | version: v1.0 2 | name: Ruby 3 | agent: 4 | machine: 5 | type: e1-standard-2 6 | os_image: ubuntu2004 7 | blocks: 8 | - name: bundle exec rspec 9 | task: 10 | jobs: 11 | - name: bundle install 12 | commands: 13 | - sudo apt install -y p7zip-full trash-cli 14 | - checkout 15 | - sem-version ruby 3.2.2 16 | - bundle install --path vendor/bundle 17 | - bundle exec rspec 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem "rspec" 3 | gem "pry" 4 | gem "optimist" 5 | gem "color" 6 | gem "rake" 7 | gem "nokogiri" 8 | gem "sqlite3" 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### Everything except `rename` script 2 | 3 | Copyright (c) 2012-2019 Tomasz Wegrzanowski 4 | 5 | MIT License 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | ### `rename` script 27 | 28 | #### Authors 29 | 30 | Aristotle Pagaltzis 31 | 32 | Idea, inspiration and original code from Larry Wall and Robin Barker. 33 | 34 | #### Copyright 35 | 36 | This script is free software; you can redistribute it and/or modify it under the same terms as Perl itself. 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | unix-utilities 2 | ============== 3 | 4 | Various small Unix utilities for ~/bin 5 | 6 | All of them have been written by me, except ~/bin/rename by Larry Wall 7 | which I'm bundling with the rest for convenience since a lot of Unix boxes don't have it. 8 | 9 | These are all meant to work on OSX. Most but not all will work on other Unix 10 | distributions. If you have patches to make them work elsewhere, send them 11 | via pull request or another convenient method. 12 | 13 | Individual utilities 14 | ==================== 15 | 16 | annotate_sgf 17 | ------------ 18 | 19 | It uses Gnu Go debug mode to annotate your go game in SGF. 20 | It will find a lot of tactical mistakes for most games by kyu players. 21 | 22 | Usage: 23 | 24 | annotate_sgf 25 | 26 | Output saved to annotated- in the same directory as . 27 | 28 | See: http://t-a-w.blogspot.com/2009/08/get-better-at-go-with-gnugo.html 29 | 30 | colcut 31 | ------ 32 | 33 | Cuts long lines to specific number of characters for easy previewing. 34 | 35 | colcut 80 < file.xml 36 | 37 | convert_to_png 38 | -------------- 39 | 40 | Converts various image formats to PNG. 41 | Mostly useful for mass conversion, for example when you have a directory 42 | with 100 svg files dir/file-001.svg to dir/file-100.svg: 43 | 44 | convert_to_png dir/*.svg 45 | 46 | will convert them all. 47 | 48 | countdown 49 | --------- 50 | 51 | Counts down time, then optionally runs a command. 52 | 53 | countdown 60 54 | countdown 60 open rickroll.mp3 55 | 56 | dedup_files 57 | ----------- 58 | 59 | Deletes duplicate files in huge directories by hash, with some optimization 60 | to avoid unnecessary hashing. 61 | 62 | Usage: 63 | 64 | dedup_files <... 65 | 66 | For example: 67 | 68 | dedup_files my_little_pony_wallpapers/ 69 | 70 | which will work pretty well even if you have 100GB of My Little Pony wallpapers. 71 | 72 | Files in eariler directories on the list, or with earlier filenames have priority to remain. 73 | 74 | diffschemas 75 | ----------- 76 | 77 | Gives diff of mysql schemas. 78 | 79 | Do dump mysql schema use: 80 | 81 | mysqldump -uuser -ppassword -h hostname --where 0=1 database >schema.sql 82 | 83 | Then run: 84 | 85 | diffschemas schema_1.sql schema_2.sql 86 | 87 | which will strip garbage like autoincrement counters and give you clean diff. 88 | 89 | e 90 | --- 91 | 92 | This utility has extremely short name since it's meant to be used as your primary 93 | way to call text editor. 94 | 95 | If you give it a path containing /, or file with such name exists in current directory, 96 | it will call your editor on that file. 97 | 98 | Otherwise - it will search your `$PATH` for this file, and execute your editor on it, 99 | avoiding opening binaries, and other false positives. 100 | 101 | This is extremely helpful if you have a ton of scripts you edit a lot. 102 | 103 | These two commands achieve similar effect: 104 | 105 | mate `which foo` 106 | 107 | e foo 108 | 109 | except `e` is shorter, doesn't force you to think about paths, 110 | will expand all symlinks in name (avoiding issues like accidentally editing the 111 | same file under different name in two editor window), and won't accidentally open binaries. 112 | 113 | Editor it will use is `$E_EDITOR`, then `$EDITOR`, then TextMate if neither variable is specified. 114 | `$E_EDITOR` variable is provided in case you want to set up them as: 115 | 116 | 117 | export E_EDITOR=mate 118 | export EDITOR="mate -w" 119 | 120 | since git and other such tools require waiting flag. 121 | 122 | flickr_find 123 | ----------- 124 | 125 | Find Creative Commons licenced photos on flickr. 126 | 127 | Usage example: 128 | 129 | flickr_find cute kittens 130 | 131 | flickr_get 132 | ---------- 133 | 134 | Download best quality version of a photo from flickr and annotate it with proper file name. 135 | 136 | Usage example: 137 | 138 | flickr_get http://www.flickr.com/photos/pagedooley/386303100/ 139 | 140 | which will be saved as `~/Downloads/naughty_cat_by_kevin_dooley_from_flickr_cc-by.jpg` 141 | 142 | fix_permissions 143 | --------------- 144 | 145 | Removes executable flag from files which shouldn't have it. 146 | Useful for archives that went through a Windows system, zip archive, 147 | or other system not aware of Unix executable flag. 148 | 149 | It doesn't turn +x flag, only removes it if a file neither starts with #!, 150 | nor is an executable according to `file` utility. 151 | 152 | Usage: 153 | 154 | fix_permissions ~/Downloads 155 | 156 | If no parameters are passed, it fixes permissions in current directory. 157 | 158 | 159 | git_hash 160 | -------- 161 | 162 | Hash contents of current git repository. It is useful when multiple branches 163 | can have same contents. 164 | 165 | Usage example: 166 | 167 | git_hash ~/repository 168 | git_hash # will hash current directory 169 | 170 | 171 | gzip_stream 172 | ----------- 173 | 174 | Pipe through it to gzip log without having infinitely long buffers. 175 | 176 | Usage example: 177 | 178 | my_server | gzip_stream > log.gz 179 | 180 | If you use regular gzip the last few hundred lines will be in memory indefinitely, 181 | so you won't be able to see what's going on in `log.gz` without killing the server, 182 | even if it happened yesterday. `gzip_stream` flushes every 5s (easily configurable), 183 | sacrificing tiny amount of compression quality for huge amount of convenience. 184 | 185 | See: http://t-a-w.blogspot.com/2010/07/synchronized-compressed-logging-unix.html 186 | 187 | json_pp 188 | ------- 189 | 190 | Pretty-prints jsons and sorts keys alphabetically. This is extremely useful 191 | as `json_pp` included in OSX completely scrambles key order, so if you hope 192 | for any kind of meaningful diff, that's not going to work. 193 | 194 | Usage example: 195 | 196 | json_pp human_readable.json 197 | 198 | kindle_sync 199 | ----------- 200 | 201 | Sync your collection of ebooks with Kindle. 202 | Handles common format conversions (epub->mobi) if you have calibre installed. 203 | 204 | Usage example: 205 | 206 | kindle_sync --report ~/Documents/Ebooks /media/Kindle/documents/Ebooks 207 | kindle_sync --sync ~/Documents/Ebooks /media/Kindle/documents/Ebooks 208 | kindle_sync --cleanup ~/Documents/Ebooks /media/Kindle/documents/Ebooks 209 | kindle_sync --list ~/Documents/Ebooks 210 | 211 | lastfm_status 212 | ------------- 213 | 214 | Find what your friends have been listening to recently. 215 | 216 | Usage example: 217 | 218 | lastfm_status some_user 219 | 220 | It requires `magic-xml` gem. 221 | 222 | media_size 223 | ---------- 224 | 225 | Calculates total size of a media directory 226 | 227 | Requires `exiftool` program. EXIF information it uses is not guaranteed to be correct. 228 | 229 | Usage: 230 | 231 | media_size some_podcasts/ 232 | media_size some_music_album/ 233 | media_size some_movie/ 234 | 235 | `-t` option also prints totals: 236 | 237 | media_size -t some_movie/ another_movie/ third_movie/ 238 | 239 | `-e` option skips empty directories in output. 240 | 241 | namenorm 242 | -------- 243 | 244 | Safely normalizes file names replacing upper case characters and spaces with 245 | lower case characters and underlines. 246 | 247 | Usage: 248 | 249 | namenorm ~/Downloads/* 250 | 251 | 252 | open_chrome 253 | ----------- 254 | 255 | Opens a a list of URLs in Chrome. It can be passed either as arguments or on input, one per line. 256 | 257 | Usage: 258 | 259 | open_chrome url1 url2 url3 260 | cat urls.txt | open_chrome 261 | 262 | open_incognito 263 | -------------- 264 | 265 | Opens a a list of URLs in Chrome, in incognito mode. It can be passed either as arguments or on input, one per line. 266 | 267 | Usage: 268 | 269 | open_incognito url1 url2 url3 270 | cat urls.txt | open_incognito 271 | 272 | open_youtube 273 | ------------ 274 | 275 | For a file downloaded from youtube (with youtube ID encoded in name), open corresponding youtube URL. 276 | 277 | Usage: 278 | 279 | open_youtube "Rick Astley - Never Gonna Give You Up-dQw4w9WgXcQ.mp4" 280 | 281 | openmany 282 | -------- 283 | 284 | Runs `open` command on multiple files, either as command line arguments, 285 | or one-per-line in STDIN. 286 | 287 | It uses OSX `open` command for OSX, or `xdg-open` on Linux. 288 | 289 | You can also pass arguments to open, either by separating arguments from files by `--` or else everything starting from `-` is considered an argument. 290 | 291 | Usage: 292 | 293 | openmany /dev/null 360 | progress -l sample.txt 401 | 402 | randsample 403 | -------- 404 | 405 | Randomly samples lines of STDIN. Count is 1 by default. 406 | 407 | Usage: 408 | 409 | seq 1 20 | randsample 1 410 | 411 | rbexe 412 | ----- 413 | 414 | Creates executable script path with proper `#!` line and permissions. 415 | 416 | Defaults to Ruby executable but supports a few other `#!`s. 417 | 418 | Usage: 419 | 420 | rbexe file.rb 421 | rbexe --9 file.rb 422 | rbexe --pl file.pl 423 | 424 | If file exists, it will only change its permissions without overwriting it, 425 | so it's safe to use. 426 | 427 | rename 428 | ------ 429 | 430 | Larry Wall's rename script, included in Debian-derived distribution, but not on any other Unix 431 | I know of - which is literally criminal, since it's one of core Unix utilities. 432 | 433 | If your distribution doesn't have it (or worse - has some total crap as `rename` script), 434 | do yourself a service and install something more sensible, and in the meantime copy this 435 | file to your `~/bin`. 436 | 437 | rjq 438 | --- 439 | 440 | Runs ruby code on JSON. A hybrid of `ruby -ple` and `jq`. 441 | 442 | Data is in `$_` and whatever is in `$_` at the end gets pretty-printed as JSON. 443 | 444 | Usage: 445 | 446 | curl -s 'https://dog.ceo/api/breeds/image/random' | rjq '$_=$_["message"]' 447 | 448 | rot13 449 | ----- 450 | 451 | ROT13 a file. 452 | 453 | Usage (either form works): 454 | 455 | rot13 double_the_security.txt 458 | 459 | 460 | sortby 461 | ------ 462 | 463 | Sort input through arbitrary Ruby expression. A lot more flexible than Unix `sort` utility. 464 | 465 | Usage: 466 | 467 | sortby '$_.length' pokemon_by_oldest.txt 525 | 526 | terminal_title 527 | -------------- 528 | 529 | Changes title of current terminal window. Extremely useful if you have too many terminal titles. 530 | 531 | Usage example: 532 | 533 | terminal_title 'Production server (do not accidentally killall -9)'; ssh production.server.example 534 | 535 | It can also change backgrounds (in iTerm2) 536 | 537 | terminal_title -c 255,0,0 'Red terminal' 538 | terminal_title --color 0,0,255 'Blue terminal' 539 | 540 | tfl_travel_time 541 | --------------- 542 | 543 | Check TfL website for travel time between two places in London. 544 | Does not handle disambiguations so you need to be fully specific (like "Victoria Underground Station" not just "Victoria") 545 | 546 | Usage example: 547 | 548 | tfl_travel_time "Victoria Underground Station" "Liverpool Street Underground Station" 549 | 550 | If the script doesn't get the answer, it opens the website (where you can disambiguate etc.) 551 | 552 | toutf8 553 | ------ 554 | 555 | Autodetects input format, and converts UTF8 / UTF16LE / UTF16BE with and without BOM into UTF8 without BOM. This allows use with Unix utilities. 556 | 557 | Usage example: 558 | 559 | toutf8 fileout.txt 560 | diff <(toutf8 "test" 2 | 3 | desc "Run tests" 4 | task "test" do 5 | sh "rspec" 6 | end 7 | -------------------------------------------------------------------------------- /bin/annotate_sgf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | level = 15 4 | ARGV.each do |sgf| 5 | annotated_sgf = File.dirname(sgf) + "/" + "annotated-" + File.basename(sgf) 6 | system *[ 7 | 'gnugo', 8 | '--level', "#{level}", 9 | '--output-flags', 'dv', 10 | '--replay', 'both', 11 | '-l', sgf, 12 | '-o', annotated_sgf, 13 | ] 14 | end 15 | -------------------------------------------------------------------------------- /bin/colcut: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | my $limit = $ARGV[0] || 120; 4 | 5 | while() { 6 | s/\r?\n$//; 7 | print(substr($_, 0, $limit), "\n"); 8 | } 9 | -------------------------------------------------------------------------------- /bin/convert_to_png: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ARGV.each do |fn| 4 | next if fn =~ /\.png\Z/ 5 | pngfn = fn.sub(/\.([a-z]+)\Z/, ".png") 6 | if File.exist?(pngfn) 7 | puts "Not converting #{fn} to #{pngfn} because target exists" 8 | next 9 | end 10 | system "convert", fn, pngfn 11 | end 12 | -------------------------------------------------------------------------------- /bin/countdown: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | unless ARGV[0] 4 | STDERR.puts "Usage: #{$0} [ ]" 5 | exit 1 6 | end 7 | 8 | def fmt_time(s) 9 | minutes = s.to_i/60 10 | "%d:%02d" % [minutes, s - minutes*60] 11 | end 12 | 13 | time_target = ARGV.shift.to_i 14 | time_start = Time.now 15 | while true 16 | time_elapsed = Time.now-time_start 17 | print "\r", fmt_time((time_target - time_elapsed).ceil) 18 | break if time_elapsed >= time_target 19 | sleep 1 20 | end 21 | print "\rSTART!\n" 22 | 23 | unless ARGV.empty? 24 | system *ARGV 25 | end 26 | -------------------------------------------------------------------------------- /bin/dedup_files: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "fileutils" 4 | require "pathname" 5 | require "digest" 6 | 7 | class DedupFiles 8 | attr_reader :files 9 | def initialize 10 | @files = [] 11 | @total = 0 12 | end 13 | def add(fn) 14 | fn = Pathname(fn) 15 | if fn.directory? 16 | fn.children.each do |cfn| 17 | add(cfn) 18 | end 19 | elsif fn.file? 20 | @files << fn 21 | end 22 | end 23 | 24 | def rm_dup_of!(file, base) 25 | @total += file.size 26 | puts "`#{file}' is a duplicate of `#{base}'" 27 | FileUtils.rm_f file 28 | end 29 | 30 | def dedup_group!(fns) 31 | by_hash = {} 32 | fns.each do |fn| 33 | d = Digest::SHA1.new.file(fn).hexdigest 34 | (by_hash[d] ||= []) << fn 35 | end 36 | by_hash.each do |h, files| 37 | keep = files.shift 38 | files.each do |fn| 39 | rm_dup_of!(fn, keep) 40 | end 41 | end 42 | end 43 | 44 | def dedup_1! 45 | by_sz_and_bn = {} 46 | @files.each do |fn| 47 | next unless fn.exist? 48 | key = [fn.size, fn.basename.to_s] 49 | (by_sz_and_bn[key] ||= []) << fn 50 | end 51 | by_sz_and_bn.values.each do |fns| 52 | dedup_group!(fns) if fns.size > 1 53 | end 54 | end 55 | 56 | def dedup_2! 57 | by_sz = {} 58 | @files.each do |fn| 59 | next unless fn.exist? 60 | (by_sz[fn.size] ||= []) << fn 61 | end 62 | by_sz.values.each do |fns| 63 | dedup_group!(fns) if fns.size > 1 64 | end 65 | end 66 | 67 | def dedup! 68 | dedup_1! 69 | dedup_2! 70 | puts "#{@total} bytes in duplicated files" if @total > 0 71 | end 72 | end 73 | 74 | if ARGV.size == 0 75 | STDERR.puts "Usage: #{$0} ..." 76 | exit 1 77 | end 78 | 79 | df = DedupFiles.new 80 | ARGV.each{|fn| df.add(fn) } 81 | 82 | df.dedup! 83 | -------------------------------------------------------------------------------- /bin/diffschemas: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require "pp" 4 | 5 | class File 6 | def self.write(path, cnt) 7 | File.open(path, 'wb'){|fh| fh.write cnt} 8 | end 9 | end 10 | 11 | def read_schema(file_name) 12 | scm = File.read(file_name) 13 | scm.gsub!(/\bdefault\b/i, "DEFAULT") 14 | scm.gsub!(/\bauto_increment\b/i, "AUTO_INCREMENT") 15 | scm.gsub!(/\bcharacter set\b/i, "CHARACTER SET") 16 | scm.gsub!(/^-- (Dump completed on|Server version|Host:) .*\n/, "") 17 | scm.gsub!(/\bAUTO_INCREMENT=\d+/i, "AUTO_INCREMENT=1") 18 | scm.gsub!(/ USING BTREE\b/, "") 19 | scm.gsub!(/[ \t]+/, " ") 20 | scm 21 | end 22 | 23 | schema_one = read_schema(ARGV[0]) 24 | schema_two = read_schema(ARGV[1]) 25 | 26 | File.write("/tmp/s1", schema_one) 27 | File.write("/tmp/s2", schema_two) 28 | system "diff", "/tmp/s1", "/tmp/s2" 29 | -------------------------------------------------------------------------------- /bin/e: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "pathname" 3 | require "shellwords" 4 | 5 | def which(a) 6 | (ENV['PATH']||"").split(":").each do |dir| 7 | begin 8 | f = Pathname(dir)+a 9 | 10.times{ f = f.readlink if f.symlink? } 10 | return f.to_s if (f.executable? and 11 | f.file? and 12 | f.open.read(2) == "#!") 13 | rescue 14 | end 15 | end 16 | a 17 | end 18 | 19 | args = ARGV.map do |a| 20 | if File.exist?(a) or a =~ %r[(\A-)|/] 21 | a 22 | else 23 | which(a) 24 | end 25 | end 26 | 27 | # Extra variable E_EDITOR in case you want EDITOR to be 'code -w' 28 | # (for git/svn/etc.) but you don't want -w flag passed to e 29 | editor = ENV["E_EDITOR"] || ENV["EDITOR"] || "code" 30 | cmd = Shellwords.split(editor) + args 31 | exec(*cmd) 32 | -------------------------------------------------------------------------------- /bin/fix_permissions: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "pathname" 4 | require "shellwords" 5 | require "find" 6 | require "fileutils" 7 | 8 | class Pathname 9 | def script? 10 | read(2) == "#!" 11 | end 12 | 13 | def file_type 14 | @file_type ||= `file -b #{self.to_s.shellescape}`.chomp 15 | end 16 | 17 | def should_be_executable? 18 | return true if script? 19 | return true if file_type =~ /\b(Mach-O|executable|ELF \d\d-bit)\b/ and not file_type =~ /dynamically linked shared library|\(DLL\)/ 20 | false 21 | rescue 22 | false 23 | end 24 | end 25 | 26 | def fix_permissions(path) 27 | Pathname(path).find do |fn| 28 | next if fn.directory? 29 | next if fn.symlink? 30 | next unless fn.executable? 31 | fn.chmod(0666 &~ File.umask) unless fn.should_be_executable? 32 | end 33 | end 34 | 35 | if ARGV.empty? 36 | fix_permissions "." 37 | else 38 | ARGV.each do |path| 39 | fix_permissions path 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /bin/flickr_find: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | url = "https://www.flickr.com/search/?text=#{ARGV.join('+')}&license=2%2C3%2C4%2C5%2C6%2C9" 4 | system "open", url 5 | -------------------------------------------------------------------------------- /bin/flickr_get: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "fileutils" 4 | require "rubygems" 5 | require "objectiveflickr" 6 | require "optimist" 7 | 8 | class FlickrGetter 9 | attr_reader :root_dir 10 | 11 | def initialize(root_dir) 12 | @root_dir = root_dir 13 | end 14 | 15 | def flickr 16 | @flickr ||= FlickrInvocation.new(api_key) 17 | end 18 | 19 | def api_key 20 | '0309264c0827bdb4ebcbff7c5b4a14a3' 21 | end 22 | 23 | def extract_photo_id(arg) 24 | if arg =~ %r[\Ahttp://farm\d+.static.flickr.com/\d+/(\d+)_.*\.jpg\Z] 25 | photo_id = $1 26 | elsif arg =~ /(\d*)\D*\Z/ 27 | photo_id = $1 28 | else 29 | raise "Parse error: #{arg}" 30 | end 31 | end 32 | 33 | # https://www.flickr.com/services/api/flickr.photos.licenses.getInfo.html 34 | def licenses 35 | { 36 | "0" => "non-free", 37 | "1" => "cc-nc-sa", 38 | "2" => "cc-nc", 39 | "3" => "cc-nc-nd", 40 | "4" => "cc-by", 41 | "5" => "cc-sa", 42 | "6" => "cc-nd", 43 | "9" => "public-domain", 44 | } 45 | end 46 | 47 | def comment_from_getinfo(r) 48 | title = r["photo"]["title"]["_content"] 49 | author = r["photo"]["owner"]["username"] 50 | license_id = r["photo"]["license"] 51 | license = licenses[license_id] or raise "Unknown license #{license_id}" 52 | "#{title} by #{author} from flickr (#{license.upcase})" 53 | end 54 | 55 | def filename_from_getinfo(r) 56 | title_fn = r["photo"]["title"]["_content"].downcase.gsub(/ /,"_").gsub(/[^a-z0-9_]/,"") 57 | author_fn = r["photo"]["owner"]["username"].downcase.gsub(/ /,"_").gsub(/[^a-z0-9_]/,"") 58 | license = licenses[r["photo"]["license"]] 59 | format = r["photo"]["originalformat"] || "jpg" 60 | "#{root_dir}/#{title_fn}_by_#{author_fn}_from_flickr_#{license}.#{format}" 61 | end 62 | 63 | def url_from_getinfo(r) 64 | server_id = r["photo"]["server"] 65 | secret = r["photo"]["originalsecret"] 66 | farm = r["photo"]["farm"] 67 | "http://farm#{farm}.static.flickr.com/#{server_id}/#{photo_id}_#{secret}_o.#{format}" 68 | end 69 | 70 | def url_from_getsizes(r) 71 | r["sizes"]["size"].max_by{|x| x["width"].to_i}["source"] 72 | end 73 | 74 | def get!(arg) 75 | photo_id = extract_photo_id(arg) 76 | r1 = flickr.call("flickr.photos.getInfo", :photo_id => photo_id) 77 | r2 = flickr.call("flickr.photos.getSizes", :photo_id => photo_id) 78 | 79 | comment = comment_from_getinfo(r1) 80 | filename = filename_from_getinfo(r1) 81 | url = url_from_getsizes(r2) 82 | 83 | if url 84 | if File.exists?(filename) 85 | puts comment 86 | puts "Already exists: #{filename}" 87 | else 88 | FileUtils.mkdir_p File.dirname(filename) 89 | system "wget", "-nv", url, "-O", filename 90 | puts url 91 | puts comment 92 | puts filename 93 | end 94 | else 95 | puts comment 96 | puts "Failed to get #{filename}" 97 | end 98 | end 99 | end 100 | 101 | opts = Optimist::options do 102 | opt :out, "Directory to save files to", :default => "#{ENV["HOME"]}/Downloads" 103 | end 104 | 105 | fg = FlickrGetter.new(opts[:out]) 106 | 107 | if ARGV.empty? 108 | STDIN.readlines.map(&:strip).each do |arg| 109 | fg.get! arg 110 | end 111 | else 112 | ARGV.each do |arg| 113 | fg.get! arg 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /bin/git_hash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "digest/sha1" 4 | 5 | def git_hash(dir) 6 | Dir.chdir(dir) do 7 | tree = [] 8 | tree << ["/submodules", `git submodule`.split(/\n/).sort] 9 | `git ls-files`.split(/\n/).each do |path| 10 | if File.directory?(path) 11 | next 12 | else 13 | tree << [path, Digest::SHA1.hexdigest(File.read(path))] 14 | end 15 | end 16 | Digest::SHA1.hexdigest(tree.sort.inspect) 17 | end 18 | end 19 | 20 | puts git_hash(ARGV[0] || ".") 21 | -------------------------------------------------------------------------------- /bin/gzip_stream: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "thread" 4 | require "zlib" 5 | 6 | def gzip_stream(io_in, io_out, flush_freq) 7 | fh = Zlib::GzipWriter.wrap(io_out) 8 | lock = Mutex.new 9 | Thread.new do 10 | while true 11 | lock.synchronize do 12 | return if fh.closed? 13 | fh.flush if fh.pos > 0 14 | end 15 | sleep flush_freq 16 | end 17 | end 18 | io_in.each do |line| 19 | lock.synchronize do 20 | fh.print(line) 21 | end 22 | end 23 | lock.synchronize do 24 | fh.close 25 | end 26 | end 27 | 28 | gzip_stream(STDIN, STDOUT, 1) 29 | -------------------------------------------------------------------------------- /bin/json_pp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "json" 4 | 5 | def json_normalize(data) 6 | if data.is_a?(Array) 7 | data.map do |elem| 8 | json_normalize(elem) 9 | end 10 | elsif data.is_a?(Hash) 11 | Hash[data.map{|k,v| 12 | [k, json_normalize(v)] 13 | }.sort] 14 | else 15 | data 16 | end 17 | end 18 | 19 | data = JSON.parse(ARGF.read) 20 | data = json_normalize(data) 21 | puts JSON.pretty_generate(data) 22 | -------------------------------------------------------------------------------- /bin/kindle_sync: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "pathname" 4 | 5 | class Pathname 6 | def without_extname 7 | Pathname(to_s.chomp(extname)) 8 | end 9 | def to_str 10 | to_s 11 | end 12 | end 13 | 14 | class Hash 15 | def hzip(other) 16 | result = {} 17 | each do |k,v| 18 | result[k] ||= [nil, nil] 19 | result[k][0] = v 20 | end 21 | other.each do |k,v| 22 | result[k] ||= [nil, nil] 23 | result[k][1] = v 24 | end 25 | result 26 | end 27 | end 28 | 29 | class BooksRepository 30 | attr_reader :path 31 | def initialize(path) 32 | @path = Pathname(path) 33 | raise "Invalid path `#{@path}'" unless @path.exist? 34 | end 35 | 36 | def files 37 | @path.find.select(&:file?).reject{|path| 38 | path.dirname.extname == ".sdr" or path.basename.to_s == ".DS_Store" 39 | }.map{|path| path.relative_path_from(@path)} 40 | end 41 | 42 | def file_groups 43 | Hash[files.group_by(&:without_extname).map do |prefix, paths| 44 | [prefix, paths.map(&:extname).sort] 45 | end] 46 | end 47 | 48 | def print! 49 | file_groups.sort.each do |prefix, exts| 50 | puts "#{prefix} (#{exts.join(" ")})" 51 | end 52 | end 53 | end 54 | 55 | class KindleSync 56 | def initialize(repo_path, device_path) 57 | @repo = BooksRepository.new(repo_path) 58 | @device = BooksRepository.new(device_path) 59 | end 60 | 61 | def books 62 | @books ||= @repo.file_groups.hzip(@device.file_groups) 63 | end 64 | 65 | def repo_state(book) 66 | books[book][0] 67 | end 68 | 69 | def device_state(book) 70 | books[book][1] 71 | end 72 | 73 | def sync_command(state1, state2) 74 | return :ok if state1 == state2 75 | return :ok if state1 != nil and state2 and state2.include?(".mobi") 76 | if state2 == nil 77 | return :copy_mobi if state1.include?(".mobi") 78 | return :copy if state1 == [".pdf"] 79 | return :convert_epub if state1 == [".epub"] 80 | return :convert_epub if state1 == [".epub", ".pdf"] 81 | end 82 | return :cleanup if state1 == nil 83 | return :unknown 84 | end 85 | 86 | def format_state(state) 87 | return "-" unless state 88 | state.sort.join(" ") 89 | end 90 | 91 | def report! 92 | books.sort.each do |book, (state1, state2)| 93 | cmd = sync_command(state1, state2) 94 | puts "#{book} (#{format_state state1} | #{format_state state2} | #{cmd})" unless cmd == :ok 95 | end 96 | end 97 | 98 | def copy_file!(source_path, target_path) 99 | raise "Already exists: `#{target_path}'" if target_path.exist? 100 | target_path.dirname.mkpath 101 | FileUtils.cp source_path, target_path 102 | end 103 | 104 | def copy_book!(book, exts_to_copy=repo_state(book)) 105 | raise if device_state(book) 106 | exts_to_copy.each do |ext| 107 | copy_file! Pathname("#{@repo.path + book}#{ext}"), Pathname("#{@device.path + book}#{ext}") 108 | end 109 | end 110 | 111 | def convert_epub!(book) 112 | raise if device_state(book) 113 | raise unless repo_state(book).include?(".epub") 114 | 115 | source_path = Pathname("#{@repo.path + book}.epub") 116 | target_path = Pathname("#{@device.path + book}.mobi") 117 | target_path.dirname.mkpath 118 | system *%W[/Applications/calibre.app/Contents/MacOS/ebook-convert #{source_path} #{target_path} --output-profile kindle_voyage] 119 | end 120 | 121 | def sync! 122 | books.sort.each do |book, (state1, state2)| 123 | cmd = sync_command(state1, state2) 124 | case cmd 125 | when :ok 126 | # OK 127 | when :cleanup 128 | # Use separate command for that 129 | when :copy 130 | copy_book!(book) 131 | when :copy_mobi 132 | copy_book!(book, [".mobi"]) 133 | when :convert_epub 134 | convert_epub!(book) 135 | when :unknown 136 | warn "Don't know how to sync this book: #{book} (#{format_state state1} | #{format_state state2} | #{cmd})" 137 | end 138 | end 139 | end 140 | 141 | def cleanup_book!(book) 142 | raise if repo_state(book) 143 | device_state(book).each do |ext| 144 | system "trash", "#{@device.path + book}#{ext}" 145 | end 146 | end 147 | 148 | def cleanup! 149 | books.sort.each do |book, (state1, state2)| 150 | cmd = sync_command(state1, state2) 151 | cleanup_book! book if cmd == :cleanup 152 | end 153 | end 154 | end 155 | 156 | case [ARGV.size, ARGV[0]] 157 | when [3, "--report"] 158 | ks = KindleSync.new(ARGV[1], ARGV[2]) 159 | ks.report! 160 | when [3, "--sync"] 161 | ks = KindleSync.new(ARGV[1], ARGV[2]) 162 | ks.sync! 163 | when [3, "--cleanup"] 164 | ks = KindleSync.new(ARGV[1], ARGV[2]) 165 | ks.cleanup! 166 | when [2, "--list"] 167 | repo = BooksRepository.new(ARGV[1]) 168 | repo.print! 169 | else 170 | STDERR.puts "Usage: #{$0} --report /repository/path /device/documents/path" 171 | STDERR.puts " or: #{$0} --sync /repository/path /device/documents/path" 172 | STDERR.puts " or: #{$0} --cleanup /repository/path /device/documents/path" 173 | STDERR.puts " or: #{$0} --list /repository/path" 174 | exit 1 175 | end 176 | -------------------------------------------------------------------------------- /bin/lastfm_status: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "open-uri" 4 | require "nokogiri" 5 | 6 | class Numeric 7 | def time_pp 8 | s = self.to_i 9 | return "#{self}s" if s < 60 10 | 11 | m = (s / 60).to_i 12 | s -= m*60 13 | return "#{m}m#{s}s" if m < 60 14 | 15 | h = (m / 60).to_i 16 | m -= h*60 17 | return "#{h}h #{m}m#{s}s" if h < 24 18 | 19 | d = (h / 24).to_i 20 | h -= d*24 21 | return "#{d}d #{h}h #{m}m#{s}s" 22 | end 23 | end 24 | 25 | users = ARGV 26 | 27 | users.each do |user_name| 28 | url = "https://www.last.fm/user/#{user_name}" 29 | begin 30 | doc = Nokogiri::HTML(URI.open(url)) 31 | last_song = doc.css(".chartlist tr")[1] 32 | rescue OpenURI::HTTPError 33 | last_song = nil 34 | end 35 | 36 | if last_song 37 | title = last_song.css(".chartlist-name a").map(&:text).first 38 | artist = last_song.css(".chartlist-artist a").map(&:text).first 39 | time_played = Time.parse(last_song.css(".chartlist-timestamp span").text) 40 | ago = Time.now - time_played 41 | print "#{user_name}'s last song was `#{title}' by `#{artist}' at #{time_played}\n" 42 | print "It was #{ago.time_pp} ago\n" 43 | else 44 | print "No recent songs by #{user_name}\n" 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /bin/media_size: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "pathname" 4 | require "shellwords" 5 | require "moneta" 6 | require "digest" 7 | require "optimist" 8 | 9 | class Pathname 10 | def shellescape 11 | to_s.shellescape 12 | end 13 | end 14 | 15 | Cache = Moneta.new(:PStore, file: "#{ENV["HOME"]}/.media_size.cache") 16 | 17 | class MediaFile < Pathname 18 | def media_file? 19 | case extname.downcase 20 | when *%W[.mp4 .avi .flv .mp3 .mpg .mkv .flv .wmv .asf .m4b] 21 | true 22 | when *%W[.sub .txt .jpg .png .srt .nfo .pdf .m3u .doc .mobi .gif .cue], "" 23 | false 24 | else 25 | `file --mime-type -b #{@path.shellescape}` =~ %r[\A(audio|video)/] 26 | end 27 | end 28 | 29 | def try_realpath 30 | (realpath rescue expand_path) 31 | end 32 | 33 | def cache_key 34 | @cache_key ||= [try_realpath.to_s, size, mtime.to_i] 35 | end 36 | 37 | # For local media files fails tend to be permanent 38 | # so we could cache them, but if you have network-mounted files 39 | # then temporary failures are possible and can be retried 40 | def duration 41 | cached = Cache[cache_key] 42 | return cached if cached 43 | computed = compute_duration 44 | # Do not cache errors 45 | Cache[cache_key] = computed unless computed == 0 46 | computed 47 | rescue 48 | warn "FAIL: #{$!}" 49 | 0 50 | end 51 | 52 | def compute_duration 53 | if extname.downcase == ".mp3" 54 | # It seems more accurate 55 | compute_duration_mp3info 56 | else 57 | compute_duration_exiftool 58 | end 59 | end 60 | 61 | def compute_duration_mp3info 62 | answer = `mp3info -p "%S" #{shellescape}` 63 | if answer.empty? 64 | # warn "Unknown size of #{@path}" 65 | 0 66 | elsif answer =~ /\A\d+\s*\z/ 67 | answer.to_i 68 | else 69 | warn "Parse error: `#{answer}'" 70 | 0 71 | end 72 | end 73 | 74 | def compute_duration_exiftool 75 | answer = `exiftool -n -s -Duration -PlayDuration #{shellescape}`.sub(" (approx)", "") 76 | if answer.empty? 77 | # warn "Unknown size of #{@path}" 78 | 0 79 | elsif answer =~ /\A(?:Track|Play)?Duration\s*:\s*(\d+\.?\d*)\s*\z/ 80 | $1.to_i 81 | else 82 | warn "Parse error: `#{answer}'" 83 | 0 84 | end 85 | end 86 | end 87 | 88 | class MediaURL 89 | def initialize(url) 90 | @url = url 91 | end 92 | 93 | def cache_key 94 | @cache_key ||= [@url] 95 | end 96 | 97 | # For URLs we do not cache fails, as that's likely a network issue and can be retried 98 | def duration 99 | cached = Cache[cache_key] 100 | return cached if cached 101 | computed = compute_duration 102 | return 0 unless computed 103 | Cache[cache_key] = computed 104 | rescue 105 | warn "FAIL: #{$!}" 106 | 0 107 | end 108 | 109 | def compute_duration 110 | answer = `youtube-dl --get-duration #{@url.shellescape}` 111 | if answer.empty? 112 | warn "Unknown size of #{@url}" 113 | nil 114 | elsif answer =~ /\A((?:\d+:)?\d?\d:\d\d\s+)+\z/ 115 | answer.split.map{|part| 116 | hms = part.split(":") 117 | hms = [0, *hms] if hms.size < 3 118 | h,m,s = hms 119 | h.to_i*3600 + m.to_i*60 + s.to_i 120 | }.inject(0, &:+) 121 | else 122 | warn "Parse error: `#{answer}'" 123 | nil 124 | end 125 | end 126 | 127 | def to_s 128 | @url 129 | end 130 | end 131 | 132 | class MediaDirectory < Pathname 133 | def media_files 134 | unless @media_files 135 | @media_files = [] 136 | retry_on_eintr do 137 | find do |file| 138 | next if file.directory? 139 | file = MediaFile.new(file) 140 | @media_files << file if file.media_file? 141 | end 142 | end 143 | end 144 | @media_files 145 | end 146 | 147 | def duration 148 | @duration ||= media_files.map(&:duration).inject(0, &:+) 149 | end 150 | 151 | # It is absolute bullshit that neither sshfs, fuse, nor ruby handles it automatically 152 | # and we have to do this in-app, but this is necessary for media_size to work over sshfs 153 | def retry_on_eintr 154 | 5.times do 155 | return yield 156 | rescue Errno::EINTR 157 | warn "EINTR, retrying" 158 | end 159 | end 160 | end 161 | 162 | class MediaSizeReporter 163 | def initialize(paths, print_totals, sorted, empty) 164 | @dirs = paths.map do |x| 165 | if x =~ /\Ahttps?:/ 166 | MediaURL.new(x) 167 | else 168 | MediaDirectory.new(x) 169 | end 170 | end 171 | @print_totals = print_totals 172 | @sorted = sorted 173 | @empty = empty 174 | end 175 | 176 | def duration 177 | @dirs.map(&:duration).inject(0, &:+) 178 | end 179 | 180 | def format_time(s) 181 | "%d:%02d:%02d" % [s/3600, s/60%60, s%60] 182 | end 183 | 184 | def report_dir!(dir) 185 | if dir.is_a?(MediaURL) 186 | puts "#{dir}: #{format_time(dir.duration)}" 187 | else 188 | return unless dir.directory? or dir.duration > 0 189 | return unless dir.duration > 0 if @empty 190 | puts "#{dir}: #{format_time(dir.duration)}" 191 | end 192 | end 193 | 194 | def each_dir(&blk) 195 | if @sorted 196 | @dirs.sort_by(&:duration).each(&blk) 197 | else 198 | @dirs.each(&blk) 199 | end 200 | end 201 | 202 | def report! 203 | each_dir do |dir| 204 | report_dir!(dir) 205 | end 206 | puts "Total: #{format_time(duration)}" if @print_totals 207 | end 208 | end 209 | 210 | opts = Optimist::options do 211 | opt :empty, "Skip empty in report" 212 | opt :totals, "Print totals" 213 | opt :sorted, "Sort by size" 214 | end 215 | 216 | if ARGV.empty? 217 | STDERR.puts "Usage: #{$0} [-t] ..." 218 | exit 1 219 | else 220 | MediaSizeReporter.new(ARGV, opts[:totals], opts[:sorted], opts[:empty]).report! 221 | end 222 | -------------------------------------------------------------------------------- /bin/namenorm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | def File.same_file?(a,b) 4 | File.stat(a) == File.stat(b) 5 | end 6 | 7 | def File.namenorm(fn) 8 | nfn = fn.tr("A-Z '", "a-z_") 9 | return if fn == nfn 10 | if !File.exist?(nfn) or File.same_file?(fn,nfn) 11 | File.rename(fn, nfn) 12 | else 13 | warn "Already exists, skipping: #{nfn}" 14 | end 15 | end 16 | 17 | files = ARGV 18 | files = files[1..-1].map{|p| Dir[p]}.flatten if files[0] == '-x' 19 | files.each{|fn| File.namenorm(fn)} 20 | -------------------------------------------------------------------------------- /bin/open_chrome: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | if ARGV.empty? 4 | urls = STDIN.readlines.map(&:chomp).reject(&:empty?) 5 | else 6 | urls = ARGV 7 | end 8 | 9 | system "open", "-na", "Google Chrome", *urls 10 | -------------------------------------------------------------------------------- /bin/open_incognito: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | if ARGV.empty? 4 | urls = STDIN.readlines.map(&:chomp).reject(&:empty?) 5 | else 6 | urls = ARGV 7 | end 8 | 9 | system "open", "-na", "Google Chrome", "--args", "--incognito", *urls 10 | -------------------------------------------------------------------------------- /bin/open_youtube: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ARGV.each do |path| 4 | # This might not be the most reliable Youtube ID extraction regexp ever 5 | youtube_id = File.basename(path).sub(/\.[^\.]+\z/, "")[/.{11}\z/] 6 | system "open", "https://www.youtube.com/watch?v=#{youtube_id}" 7 | end 8 | -------------------------------------------------------------------------------- /bin/openmany: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | def open_bin 4 | case RUBY_PLATFORM 5 | when /linux/ 6 | 'xdg-open' 7 | when /darwin/ 8 | 'open' 9 | else 10 | 'open' 11 | end 12 | end 13 | 14 | def run_open(path, open_args) 15 | path = path.strip 16 | return if path.empty? 17 | system open_bin, *open_args, path 18 | end 19 | 20 | open_args = [] 21 | 22 | if ARGV.include?("--") 23 | while arg = ARGV.shift 24 | break if arg == "--" 25 | open_args << arg 26 | end 27 | else 28 | while ARGV[0] =~ /\A-/ 29 | open_args << ARGV.shift 30 | end 31 | end 32 | 33 | if ARGV.empty? 34 | STDIN.each do |path| 35 | run_open path, open_args 36 | end 37 | else 38 | ARGV.each do |path| 39 | run_open path, open_args 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /bin/osx_screensaver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | system "open", "/System/Library/Frameworks/ScreenSaver.framework/Versions/A/Resources/ScreenSaverEngine.app" -------------------------------------------------------------------------------- /bin/osx_suspend: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | system "/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession", "-suspend" 4 | -------------------------------------------------------------------------------- /bin/pomodoro: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "shellwords" 4 | 5 | def osa(cmd) 6 | cmd = %W[osascript -e #{cmd}].map(&:shellescape).join(" ") 7 | `#{cmd}` 8 | end 9 | 10 | def mplayer(path) 11 | system *%W[mplayer -really-quiet #{path}] 12 | end 13 | 14 | def with_volume(volume) 15 | old_volume = osa("output volume of (get volume settings)").strip 16 | osa("set volume output volume #{volume}") 17 | yield 18 | ensure 19 | osa("set volume output volume #{old_volume}") 20 | end 21 | 22 | path = "/System/Library/Sounds/Ping.aiff" 23 | 24 | minutes = ARGV[0] ? ARGV[0].to_i : 25 25 | 26 | minutes.times do |i| 27 | puts "Minutes to go: #{minutes-i}" 28 | sleep 60 29 | end 30 | with_volume(100) do 31 | mplayer(path) 32 | end 33 | -------------------------------------------------------------------------------- /bin/process_gplus_takeout: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "rubygems" 4 | require "pathname" 5 | require "hpricot" 6 | 7 | class GooglePlusDocument 8 | def initialize(path) 9 | @path = path 10 | end 11 | def doc 12 | @doc ||= Hpricot(File.open(@path, 'rb', &:read)) 13 | end 14 | def published 15 | @published ||= doc.at(".published")["title"] 16 | end 17 | def published_pretty 18 | @published_pretty ||= doc.at(".published").inner_text 19 | end 20 | def content 21 | @original_content ||= doc.at(".entry-content").to_s 22 | end 23 | def permalink 24 | @permalink ||= doc.at(".permalink a")["href"] 25 | end 26 | def style 27 | doc.at("style").to_s 28 | end 29 | def remove_doc! 30 | # Free memory 31 | @doc = nil 32 | end 33 | def attachments 34 | @attachments ||= (doc/".attachment").to_a.map(&:to_s) 35 | end 36 | def parse! 37 | permalink 38 | content 39 | attachments 40 | published 41 | published_pretty 42 | end 43 | end 44 | 45 | def process_gplus_takeout!(stream_dir, output_html) 46 | docs = [] 47 | style = nil 48 | Pathname(stream_dir).children.each do |file| 49 | puts file 50 | doc = GooglePlusDocument.new(file) 51 | doc.parse! 52 | style ||= doc.style 53 | doc.remove_doc! 54 | docs << doc 55 | end 56 | File.open(output_html, 'wb') do |fh| 57 | fh.puts "" 58 | fh.puts "" 59 | fh.puts '' 60 | fh.puts style 61 | fh.puts "" 62 | fh.puts "" 63 | docs.sort_by(&:published).reverse.each do |doc| 64 | fh.puts "

#{doc.published_pretty}

" 65 | fh.puts doc.content 66 | doc.attachments.each{|at| 67 | fh.puts at 68 | end 69 | end 70 | fh.puts "" 71 | fh.puts "" 72 | } 73 | end 74 | 75 | unless ARGV.size == 2 76 | STDERR.puts "Usage: #{$0} Stream/ output.html" 77 | exit 78 | end 79 | 80 | stream_dir, output_html = *ARGV 81 | process_gplus_takeout!(stream_dir, output_html) 82 | -------------------------------------------------------------------------------- /bin/progress: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | STDERR.sync = true 4 | 5 | $bytes = true 6 | $max = nil 7 | $count = 0 8 | 9 | until ARGV.empty? 10 | case (arg = ARGV.shift) 11 | when '-l' 12 | $bytes = false 13 | when '-b' 14 | $bytes = true 15 | when /\A(\d+)([kmg]?)\Z/ 16 | units = {'k'=>2**10, 'm'=>2**20, 'g'=>2**10, ""=>1} 17 | $max = $1.to_i * units[$2] 18 | else 19 | raise "Unrecognized argument: `#{arg}'" 20 | end 21 | end 22 | 23 | $max = STDIN.stat.size if $bytes and STDIN.stat.file? and $max.nil? 24 | 25 | Thread.new do 26 | last_count = nil 27 | while true 28 | if $count != last_count 29 | if $max 30 | STDERR.print "\r#{$count}/#{$max} [#{$count*100/$max}%]" 31 | else 32 | STDERR.print "\r#{$count}" 33 | end 34 | last_count = $count 35 | end 36 | sleep 1 37 | end 38 | end 39 | 40 | begin 41 | while data = ($bytes ? STDIN.read(2**12) : STDIN.gets) 42 | STDOUT.print(data) 43 | $count += $bytes ? data.length : 1 44 | end 45 | STDERR.print "\n" 46 | rescue Errno::EPIPE 47 | end 48 | -------------------------------------------------------------------------------- /bin/pub: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | class File 4 | def self.pub(fn) 5 | unless exist?(fn) 6 | warn "File #{fn} doesn't exist" 7 | return 8 | end 9 | chmod(executable?(fn) ? 0o755 : 0o644, fn) 10 | if directory?(fn) 11 | Dir.open(fn) do |dh| 12 | dh.each do |sfn| 13 | pub(fn + "/" + sfn) unless sfn == "." or sfn == ".." 14 | end 15 | end 16 | end 17 | end 18 | end 19 | 20 | ARGV.each do |fn| 21 | File.pub(fn) 22 | end 23 | -------------------------------------------------------------------------------- /bin/rand_passwd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 56.4 bits of entropy 4 | c = ("a".."z").to_a 5 | puts (0...12).map{ c[rand(c.size)] }.join 6 | -------------------------------------------------------------------------------- /bin/randsample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | if ARGV[0] 4 | n = Integer(ARGV[0]) 5 | else 6 | n = 1 7 | end 8 | 9 | buf = STDIN.readlines.sample(n) 10 | buf.each do |line| 11 | puts line 12 | end 13 | -------------------------------------------------------------------------------- /bin/randswap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | trap("PIPE", "EXIT") 4 | buf = STDIN.readlines.shuffle 5 | buf.each do |line| 6 | puts line.chomp 7 | end 8 | -------------------------------------------------------------------------------- /bin/rbexe: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "fileutils" 4 | 5 | types = { 6 | 'ruby' => "#!/usr/bin/env ruby", 7 | 'ruby1.8' => "#!/usr/bin/env ruby1.8", 8 | 'ruby1.9' => "#!/usr/bin/env ruby1.9", 9 | 'python' => "#!/usr/bin/env python", 10 | 'python2' => "#!/usr/bin/env python2", 11 | 'python3' => "#!/usr/bin/env python3", 12 | 'perl' => "#!/usr/bin/perl", 13 | 'bash' => "#!/bin/bash", 14 | } 15 | types['rb'] = types['ruby'] 16 | types['8'] = types['rb8'] = types['ruby1.8'] 17 | types['9'] = types['rb9'] = types['ruby1.9'] 18 | types['py'] = types['python'] 19 | types['py2'] = types['python2'] 20 | types['py3'] = types['python3'] 21 | types['pl'] = types['perl'] 22 | types['sh'] = types['bash'] 23 | 24 | type = 'ruby' 25 | if ARGV[0] =~ /\A--?(.*)/ 26 | ARGV.shift 27 | type = $1 28 | end 29 | 30 | bin = types[type] or raise "No such type: #{type}" 31 | 32 | ARGV.each do |file_name| 33 | if File.exist?(file_name) 34 | warn "Already exists, chmodding only: #{file_name}" 35 | system "chmod", "+x", file_name 36 | else 37 | File.open(file_name, IO::WRONLY|IO::CREAT|IO::EXCL, 0755) do |fh| 38 | fh.puts bin 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /bin/rename: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | 5 | use Getopt::Long 2.24, qw( :config bundling no_ignore_case no_auto_abbrev ); 6 | 7 | my ( $N, $EXT, @EXT, @USE, $DECODE, $ENCODE ); 8 | sub compile { eval shift } # defined early to control the lexical environment 9 | 10 | my $msglevel = 0; 11 | sub ERROR (&) { print STDERR $_[0]->(), "\n" if $msglevel > -1 }; 12 | sub INFO (&) { print STDERR $_[0]->(), "\n" if $msglevel > 0 }; 13 | sub DEBUG (&) { print STDERR $_[0]->(), "\n" if $msglevel > 1 }; 14 | 15 | sub pod2usage { require Pod::Usage; goto &Pod::Usage::pod2usage } 16 | sub mkpath { require File::Path; goto &File::Path::mkpath } 17 | sub dirname { require File::Basename; goto &File::Basename::dirname } 18 | 19 | use constant VERB_FOR => { 20 | link => { 21 | inf => 'link', 22 | pastp => 'linked', 23 | exec => sub { link shift, shift }, 24 | }, 25 | symlink => { 26 | inf => 'symlink', 27 | pastp => 'symlinked', 28 | exec => sub { symlink shift, shift }, 29 | }, 30 | rename => { 31 | inf => 'rename', 32 | pastp => 'renamed', 33 | exec => sub { rename shift, shift }, 34 | }, 35 | }; 36 | 37 | sub argv_to_subst_expr { 38 | my $modifier = shift || ''; 39 | pod2usage( -verbose => 1 ) if @ARGV < 2; 40 | my ($from, $to) = map quotemeta, splice @ARGV, 0, 2; 41 | # the ugly \${\""} construct is necessary because unknown backslash escapes are 42 | # not treated the same in pattern- vs doublequote-quoting context; only the 43 | # latter lets us do the right thing with problematic input like 44 | # ']{ool(haracter$' or maybe '>>' 45 | sprintf 's/\Q${\"%s"}/%s/%s', $from, $to, $modifier; 46 | } 47 | 48 | sub pipe_through { 49 | my ( $cmd ) = @_; 50 | IPC::Open2::open2( my $in, my $out, $cmd ) or do { 51 | warn "couldn't open pipe to $cmd: $!\n"; 52 | return; 53 | }; 54 | print $out $_; 55 | close $out; 56 | $_ = <$in>; 57 | chomp; 58 | close $in; 59 | } 60 | 61 | my ( $VERB, @EXPR ); 62 | 63 | my %library = ( 64 | camelcase => 's/([[:alpha:]]+)/\u$1/g', 65 | urlesc => 's/%([0-9A-F][0-9A-F])/chr hex $1/ieg', 66 | nows => 's/[_[:blank:]]+/_/g', 67 | rews => 'y/_/ /', 68 | noctrl => 's/[_[:cntrl:]]+/_/g', 69 | nometa => 'tr/_!"&()=?`*\':;<>|$/_/s', 70 | trim => 's/\A[ _]+//, s/[ _]+\z//' 71 | ); 72 | 73 | GetOptions( 74 | 'h|help' => sub { pod2usage() }, 75 | 'man' => sub { pod2usage( -verbose => 2 ) }, 76 | '0|null' => \my $opt_null, 77 | 'f|force' => \my $opt_force, 78 | 'g|glob' => \my $opt_glob, 79 | 'i|interactive' => \my $opt_interactive, 80 | 'k|backwards|reverse-order' => \my $opt_backwards, 81 | 'l|symlink' => sub { $VERB ? pod2usage( -verbose => 1 ) : ( $VERB = VERB_FOR->{ 'symlink' } ) }, 82 | 'L|hardlink' => sub { $VERB ? pod2usage( -verbose => 1 ) : ( $VERB = VERB_FOR->{ 'link' } ) }, 83 | 'M|use=s' => \@USE, 84 | 'n|just-print|dry-run' => \my $opt_dryrun, 85 | 'N|counter-format=s' => \my $opt_ntmpl, 86 | 'p|mkpath|make-dirs' => \my $opt_mkpath, 87 | 'stdin!' => \my $opt_stdin, 88 | 't|sort-time' => \my $opt_time_sort, 89 | 'T|transcode=s' => \my $opt_transcode, 90 | 'v|verbose+' => \$msglevel, 91 | 92 | 'a|append=s' => sub { push @EXPR, "\$_ .= qq[${\quotemeta $_[1]}]" }, 93 | 'A|prepend=s' => sub { push @EXPR, "substr \$_, 0, 0, qq[${\quotemeta $_[1]}]" }, 94 | 'c|lower-case' => sub { push @EXPR, 's/([[:upper:]]+)/\L$1/g' }, 95 | 'C|upper-case' => sub { push @EXPR, 's/([[:lower:]]+)/\U$1/g' }, 96 | 'd|delete=s' => sub { push @EXPR, "s/${\quotemeta $_[1]}//" }, 97 | 'D|delete-all=s' => sub { push @EXPR, "s/${\quotemeta $_[1]}//g" }, 98 | 'e|expr=s' => \@EXPR, 99 | 'P|pipe=s' => sub { require IPC::Open2; push @EXPR, "pipe_through '\Q$_[1]\E'" }, 100 | 's|subst' => sub { push @EXPR, argv_to_subst_expr }, 101 | 'S|subst-all' => sub { push @EXPR, argv_to_subst_expr('g') }, 102 | 'x|remove-extension' => sub { push @EXPR, 's/\. [^.]+ \z//x' }, 103 | 'X|keep-extension' => sub { push @EXPR, 's/\.([^.]+)\z//x and do { push @EXT, $1; $EXT = join ".", reverse @EXT }' }, 104 | 'z|sanitize' => sub { push @EXPR, @library{ qw( nows noctrl nometa trim ) } }, 105 | 106 | map { my $recipe = $_; $recipe => sub { push @EXPR, $library{ $recipe } } } keys %library, 107 | ) or pod2usage(); 108 | 109 | $opt_stdin = @ARGV ? 0 : 1 unless defined $opt_stdin; 110 | 111 | $VERB ||= VERB_FOR->{ 'rename' }; 112 | 113 | if ( not @EXPR ) { 114 | pod2usage() if not @ARGV or -e $ARGV[0]; 115 | push @EXPR, shift; 116 | } 117 | 118 | pod2usage( -message => 'Error: --stdin and filename arguments are mutually exclusive' ) 119 | if $opt_stdin and @ARGV; 120 | 121 | pod2usage( -message => 'Error: --null only permitted when reading filenames from STDIN' ) 122 | if $opt_null and not $opt_stdin; 123 | 124 | pod2usage( -message => 'Error: --interactive and --force are mutually exclusive' ) 125 | if $opt_interactive and $opt_force; 126 | 127 | my $n = 1; 128 | my $nwidth = 0; 129 | if ( defined $opt_ntmpl ) { 130 | $opt_ntmpl =~ /\A(?:(\.\.\.0)|(0+))([0-9]+)\z/ 131 | or pod2usage( -message => "Error: unparseable counter format $opt_ntmpl" ); 132 | $nwidth = ( 133 | defined $1 ? -1 : 134 | defined $2 ? length $opt_ntmpl : 135 | 0 136 | ); 137 | $n = $3; 138 | } 139 | 140 | ++$msglevel if $opt_dryrun; 141 | 142 | my $code = do { 143 | if ( $opt_transcode ) { 144 | require Encode; 145 | my ( $in_enc, $out_enc ) = split /:/, $opt_transcode, 2; 146 | $DECODE = Encode::find_encoding( $in_enc ); 147 | die "No such encoding $in_enc\n" if not ref $DECODE; 148 | $ENCODE = defined $out_enc ? Encode::find_encoding( $out_enc ) : $ENCODE; 149 | die "No such encoding $out_enc\n" if not ref $ENCODE; 150 | unshift @EXPR, '$_ = $DECODE->decode($_)'; 151 | push @EXPR, '$_ = $ENCODE->encode($_)'; 152 | } 153 | 154 | my $i = $#USE; 155 | for ( reverse @USE ) { 156 | s/\A([^=]+)=?//; 157 | my $use = "use $1"; 158 | $use .= ' split /,/, $USE['.$i--.']' if length; 159 | unshift @EXPR, $use; 160 | } 161 | 162 | if ( eval 'require feature' and $^V =~ /^v(5\.[1-9][0-9]+)/ ) { 163 | unshift @EXPR, "use feature ':$1'"; 164 | } 165 | 166 | my $cat = sprintf 'sub { %s }', join '; ', @EXPR; 167 | DEBUG { "Using expression: $cat" }; 168 | 169 | my $evaled = compile $cat; 170 | die $@ if $@; 171 | die "Evaluation to subref failed. Check expression using -nv\n" 172 | unless 'CODE' eq ref $evaled; 173 | 174 | $evaled; 175 | }; 176 | 177 | if ( $opt_stdin ) { 178 | local $/ = $/; 179 | INFO { "Reading filenames from STDIN" }; 180 | @ARGV = do { 181 | if ( $opt_null ) { 182 | INFO { "Splitting on NUL bytes" }; 183 | $/ = chr 0; 184 | } 185 | ; 186 | }; 187 | chomp @ARGV; 188 | } 189 | 190 | @ARGV = map glob, @ARGV if $opt_glob; 191 | 192 | if ( $opt_time_sort ) { 193 | my @mtime = map { (stat)[9] } @ARGV; 194 | @ARGV = @ARGV[ sort { $mtime[$a] <=> $mtime[$b] } 0 .. $#ARGV ]; 195 | } 196 | 197 | @ARGV = reverse @ARGV if $opt_backwards; 198 | 199 | $nwidth = length $n+@ARGV if $nwidth < 0; 200 | 201 | for ( @ARGV ) { 202 | my $old = $_; 203 | 204 | $N = sprintf '%0*d', $nwidth, $n++; 205 | $code->(); 206 | $_ = join '.', $_, reverse splice @EXT if @EXT; 207 | 208 | if ( $old eq $_ ) { 209 | DEBUG { "'$old' unchanged" }; 210 | next; 211 | } 212 | 213 | if ( !$opt_force and -e ) { 214 | ERROR { "'$old' not $VERB->{pastp}: '$_' already exists" }; 215 | next; 216 | } 217 | 218 | if ( $opt_dryrun ) { 219 | INFO { "'$old' would be $VERB->{pastp} to '$_'" }; 220 | next; 221 | } 222 | 223 | if ( $opt_interactive ) { 224 | print "\u$VERB->{inf} '$old' to '$_'? [n] "; 225 | if ( !~ /^y(?:es)?$/i ) { 226 | DEBUG { "Skipping '$old'." }; 227 | next; 228 | } 229 | } 230 | 231 | my ( $success, @made_dirs ); 232 | 233 | ++$success if $VERB->{ 'exec' }->( $old, $_ ); 234 | 235 | if ( !$success and $opt_mkpath ) { 236 | @made_dirs = mkpath( [ dirname( $_ ) ], $msglevel > 1, 0755 ); 237 | ++$success if $VERB->{ 'exec' }->( $old, $_ ); 238 | } 239 | 240 | if ( !$success ) { 241 | ERROR { "Can't $VERB->{inf} '$old' to '$_': $!" }; 242 | rmdir $_ for reverse @made_dirs; 243 | next; 244 | } 245 | 246 | INFO { "'$old' $VERB->{pastp} to '$_'" }; 247 | } 248 | 249 | __END__ 250 | 251 | =head1 NAME 252 | 253 | rename - renames multiple files 254 | 255 | =head1 VERSION 256 | 257 | version 1.600 258 | 259 | =head1 SYNOPSIS 260 | 261 | F 262 | B<[switches|transforms]> 263 | B<[files]> 264 | 265 | Switches: 266 | 267 | =over 1 268 | 269 | =item B<-0>/B<--null> (when reading from STDIN) 270 | 271 | =item B<-f>/B<--force>EorEB<-i>/B<--interactive> (proceed or prompt when overwriting) 272 | 273 | =item B<-g>/B<--glob> (expand C<*> etc. in filenames, useful in WindowsE F) 274 | 275 | =item B<-k>/B<--backwards>/B<--reverse-order> 276 | 277 | =item B<-l>/B<--symlink>EorEB<-L>/B<--hardlink> 278 | 279 | =item B<-M>/B<--use=I> 280 | 281 | =item B<-n>/B<--just-print>/B<--dry-run> 282 | 283 | =item B<-N>/B<--counter-format> 284 | 285 | =item B<-p>/B<--mkpath>/B<--make-dirs> 286 | 287 | =item B<--stdin>/B<--no-stdin> 288 | 289 | =item B<-t>/B<--sort-time> 290 | 291 | =item B<-T>/B<--transcode=I> 292 | 293 | =item B<-v>/B<--verbose> 294 | 295 | =back 296 | 297 | Transforms, applied sequentially: 298 | 299 | =over 1 300 | 301 | =item B<-a>/B<--append=I> 302 | 303 | =item B<-A>/B<--prepend=I> 304 | 305 | =item B<-c>/B<--lower-case> 306 | 307 | =item B<-C>/B<--upper-case> 308 | 309 | =item B<-d>/B<--delete=I> 310 | 311 | =item B<-D>/B<--delete-all=I> 312 | 313 | =item B<-e>/B<--expr=I> 314 | 315 | =item B<-P>/B<--pipe=I> 316 | 317 | =item B<-s>/B<--subst I I> 318 | 319 | =item B<-S>/B<--subst-all I I> 320 | 321 | =item B<-x>/B<--remove-extension> 322 | 323 | =item B<-X>/B<--keep-extension> 324 | 325 | =item B<-z>/B<--sanitize> 326 | 327 | =item B<--camelcase>EB<--urlesc>EB<--nows>EB<--rews>EB<--noctrl>EB<--nometa>EB<--trim> (see manual) 328 | 329 | =back 330 | 331 | =head1 DESCRIPTION 332 | 333 | This program renames files according to modification rules specified on the command line. If no filenames are given on the command line, a list of filenames will be expected on standard input. 334 | 335 | The documentation contains a L. 336 | 337 | =head1 OPTIONS 338 | 339 | =head2 Switches 340 | 341 | =over 4 342 | 343 | =item B<-h>, B<--help> 344 | 345 | See a synopsis. 346 | 347 | =item B<--man> 348 | 349 | Browse the manpage. 350 | 351 | =item B<-0>, B<--null> 352 | 353 | When reading file names from C, split on NUL bytes instead of newlines. This is useful in combination with GNU find's C<-print0> option, GNU grep's C<-Z> option, and GNU sort's C<-z> option, to name just a few. B 354 | 355 | =item B<-f>, B<--force> 356 | 357 | Rename even when a file with the destination name already exists. 358 | 359 | =item B<-g>, B<--glob> 360 | 361 | Glob filename arguments. This is useful if you're using a braindead shell such as F which won't expand wildcards on behalf of the user. 362 | 363 | =item B<-i>, B<--interactive> 364 | 365 | Ask the user to confirm every action before it is taken. 366 | 367 | =item B<-k>, B<--backwards>, B<--reverse-order> 368 | 369 | Process the list of files in reverse order, last file first. This prevents conflicts when renaming files to names which are currently taken but would be freed later during the process of renaming. 370 | 371 | =item B<-l>, B<--symlink> 372 | 373 | Create symlinks from the new names to the existing ones, instead of renaming the files. B.> 374 | 375 | =item B<-L>, B<--hardlink> 376 | 377 | Create hard links from the new names to the existing ones, instead of renaming the files. B.> 378 | 379 | =item B<-M>, B<--use> 380 | 381 | Like perl's own C<-M> switch. Loads the named modules at the beginning of the rename, and can pass import options separated by commata after an equals sign, i.e. C will pass the C and C import options to C. 382 | 383 | You may load multiple modules by using this option multiple times. 384 | 385 | =item B<-n>, B<--dry-run>, B<--just-print> 386 | 387 | Show how the files would be renamed, but don't actually do anything. 388 | 389 | =item B<-N>/B<--counter-format> 390 | 391 | Format and set the C<$N> counter variable according to the given template. 392 | 393 | E.g. C<-N 001> will make C<$N> start at 1 and be zero-padded to 3 digits, whereas C<-N 0099> will start the counter at 99 and zero-pad it to 4 digits. And so forth. Only digits are allowed in this simple form. 394 | 395 | As a special form, you can prefix the template with C<...0> to indicate that C should determine the width automatically based upon the number of files. E.g. if you pass C<-N ...01> along with 300 files, C<$N> will range from C<001> to C<300>. 396 | 397 | =item B<-p>, B<--mkpath>, B<--make-dirs> 398 | 399 | Create any non-existent directories in the target path. This is very handy if you want to scatter a pile of files into subdirectories based on some part of their name (eg. the first two letters or the extension): you don't need to make all necessary directories beforehand, just tell C to create them as necessary. 400 | 401 | =item B<--stdin>, B<--no-stdin> 402 | 403 | Always E or never E read the list of filenames from STDIN; do not guess based on the presence or absence of filename arguments. B.> 404 | 405 | =item B<-T>, B<--transcode> 406 | 407 | Decode each filename before processing and encode it after processing, according to the given encoding supplied. 408 | 409 | To encode output in a different encoding than input was decoded, supply two encoding names, separated by a colon, e.g. C<-T latin1:utf-8>. 410 | 411 | Only the last C<-T> parameter on the command line is effective. 412 | 413 | =item B<-v>, B<--verbose> 414 | 415 | Print additional information about the operations (not) executed. 416 | 417 | =back 418 | 419 | =head2 Transforms 420 | 421 | Transforms are applied to filenames sequentially. You can use them multiple times; their effects will accrue. 422 | 423 | =over 4 424 | 425 | =item B<-a>, B<--append> 426 | 427 | Append the string argument you supply to every filename. 428 | 429 | =item B<-A>, B<--prepend> 430 | 431 | Prepend the string argument you supply to every filename. 432 | 433 | =item B<-c>, B<--lower-case> 434 | 435 | Convert file names to all lower case. 436 | 437 | =item B<-C>, B<--upper-case> 438 | 439 | Convert file names to all upper case. 440 | 441 | =item B<-e>, B<--expr> 442 | 443 | The C argument to this option should be a Perl expression that assumes the filename in the C<$_> variable and modifies it for the filenames to be renamed. When no other C<-c>, C<-C>, C<-e>, C<-s>, or C<-z> options are given, you can omit the C<-e> from infront of the code. 444 | 445 | =item B<-P>, B<--pipe> 446 | 447 | Pass the filename to an external command on its standard input and read back the transformed filename on its standard output. 448 | 449 | =item B<-s>, B<--subst> 450 | 451 | Perform a simple textual substitution of C to C. The C and C parameters must immediately follow the argument. 452 | 453 | =item B<-S>, B<--subst-all> 454 | 455 | Same as C<-s>, but replaces I instance of the C text by the C text. 456 | 457 | =item B<-x>, B<--remove-extension> 458 | 459 | Remove the last extension from a filename, if there is any. 460 | 461 | =item B<-X>, B<--keep-extension> 462 | 463 | Save and remove the last extension from a filename, if there is any. The saved extension will be appended back to the filename at the end of the rest of the operations. 464 | 465 | Repeating this option will save multiple levels of extension in the right order. 466 | 467 | =item B<-z>, B<--sanitize> 468 | 469 | A shortcut for passing C<--nows --noctrl --nometa --trim>. 470 | 471 | =item B<--camelcase> 472 | 473 | Capitalise every separate word within the filename. 474 | 475 | =item B<--urlesc> 476 | 477 | Decode URL-escaped filenames, such as wget(1) used to produce. 478 | 479 | =item B<--nows> 480 | 481 | Replace all sequences of whitespace in the filename with single underscore characters. 482 | 483 | =item B<--rews> 484 | 485 | Reverse C<--nows>: replace each underscore in the filename with a space. 486 | 487 | =item B<--noctrl> 488 | 489 | Replace all sequences of control characters in the filename with single underscore characters. 490 | 491 | =item B<--nometa> 492 | 493 | Replace every shell meta-character with an underscore. 494 | 495 | =item B<--trim> 496 | 497 | Remove any sequence of spaces and underscores at the left and right ends of the filename. 498 | 499 | =back 500 | 501 | =head1 VARIABLES 502 | 503 | These predefined variables are available for use within any C<-e> expressions you pass. 504 | 505 | =over 4 506 | 507 | =item B<$N> 508 | 509 | A counter that increments for each file in the list. By default, counts up from 1. 510 | 511 | The C<-N> switch takes a template that specifies the padding and starting value of C<$N>; see L. 512 | 513 | =item B<$EXT> 514 | 515 | A string containing the accumulated extensions saved by C<-X> switches, without a leading dot. See L. 516 | 517 | =item B<@EXT> 518 | 519 | An array containing the accumulated extensions saved by C<-X> switches, from right to left, without any dots. 520 | 521 | The right-most extension is always C<$EXT[0]>, the left-most (if any) is C<$EXT[-1]>. 522 | 523 | =back 524 | 525 | =head1 TUTORIAL 526 | 527 | F takes a list of filenames, runs a list of modification rules against each filename, checks if the result is different from the original filename, and if so, renames the file. The most I way to use it is to pass a line of Perl code as the rule; the most I way is to employ the many switches available to supply rules for common tasks such as stripping extensions. 528 | 529 | For example, to strip the extension from all C<.bak> files, you might use either of these command lines: 530 | 531 | rename -x *.bak 532 | rename 's/\.bak\z//' * 533 | 534 | These do not achive their results in exactly the same way: the former only takes the files that match C<*.bak> in the first place, then strips their last extension; the latter takes all files and strips a C<.bak> from the end of those filenames that have it. As another alternative, if you are confident that none of the filenames has C<.bak> anywhere else than at the end, you might instead choose to write the latter approach using the C<-s> switch: 535 | 536 | rename -s .bak '' * 537 | 538 | Of course you can do multiple changes in one go: 539 | 540 | rename -s .tgz .tar.gz -s .tbz2 .tar.bz2 *.t?z* 541 | 542 | But note that transforms are order sensitive. The following will not do what you probably meant: 543 | 544 | rename -s foo bar -s bar baz * 545 | 546 | Because rules are cumulative, this would first substitute F with F; in the resulting filenames, it would then substitute F with F. So in most cases, it would end up substituting F with F E probably not your intention. So you need to consider the order of rules. 547 | 548 | If you are unsure that your modification rules will do the right thing, try doing a verbose dry run to check what its results would be. A dry run is requested by passing C<-n>: 549 | 550 | rename -n -s bar baz -s foo bar * 551 | 552 | You can combine the various transforms to suit your needs. E.g. files from MicrosoftE WindowsE systems often have blanks and (sometimes nothing but) capital letters in their names. Let's say you have a heap of such files to clean up, I you also want to move them to subdirectories based on extension. The following command will do this for you: 553 | 554 | rename -p -c -z -X -e '$_ = "$EXT/$_" if @EXT' * 555 | 556 | Here, C<-p> tells F to create directories if necessary; C<-c> tells it to lower-case the filename; C<-X> remembers the file extension in the C<$EXT> and C<@EXT> variables; and finally, the C<-e> expression uses those to prepend the extension to the filename as a directory, I there is one. 557 | 558 | That brings us to the secret weapon in F's arsenal: the C<-X> switch. This is a transform that clips the extension off the filename and stows it away at that point during the application of the rules. After all rules are finished, the remembered extension is appended back onto the filename. (You can use multiple C<-X> switches, and they will accumulate multiple extensions as you would expect.) This allows you to do use simple way for doing many things that would get much trickier if you had to make sure to not affect the extension. E.g.: 559 | 560 | rename -X -c --rews --camelcase --nows * 561 | 562 | This will uppercase the first letter of every word in every filename while leaving its extension exactly as before. Or, consider this: 563 | 564 | rename -N ...01 -X -e '$_ = "File-$N"' * 565 | 566 | This will throw away all the existing filenames and simply number the files from 1 through however many files there are E except that it will preserve their extensions. 567 | 568 | Incidentally, note the C<-N> switch and the C<$N> variable used in the Perl expression. See L and L for documentation. 569 | 570 | =head1 COOKBOOK 571 | 572 | Using the C<-M> switch, you can quickly put F to use for just about everything the CPAN offers: 573 | 574 | =head3 Coarsely latinize a directory full of files with non-Latin characters 575 | 576 | rename -T utf8 -MText::Unidecode '$_ = unidecode $_' * 577 | 578 | See L. 579 | 580 | =head3 Sort a directory of pictures into monthwise subdirectories 581 | 582 | rename -p -MImage::EXIF '$_ = "$1-$2/$_" if Image::EXIF->new->file_name($_) 583 | ->get_image_info->{"Image Created"} =~ /(\d\d\d\d):(\d\d)/' *.jpeg 584 | 585 | See L. 586 | 587 | =head1 SEE ALSO 588 | 589 | mv(1), perl(1), find(1), grep(1), sort(1) 590 | 591 | =head1 BUGS 592 | 593 | None currently known. 594 | 595 | =head1 AUTHORS 596 | 597 | Aristotle Pagaltzis 598 | 599 | Idea, inspiration and original code from Larry Wall and Robin Barker. 600 | 601 | =head1 COPYRIGHT 602 | 603 | This script is free software; you can redistribute it and/or modify it under the same terms as Perl itself. 604 | -------------------------------------------------------------------------------- /bin/rjq: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "json" 4 | code = ARGV[0] 5 | 6 | $_ = JSON.parse(STDIN.read) 7 | eval(code) 8 | puts JSON.pretty_generate($_) 9 | -------------------------------------------------------------------------------- /bin/rot13: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ARGF.each do |line| 4 | print line.tr("a-zA-Z", "n-za-mN-ZA-M") 5 | end 6 | -------------------------------------------------------------------------------- /bin/sortby: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | code = ARGV[0] 4 | puts *STDIN.readlines.sort_by{|x| $_=x; eval(code) } 5 | -------------------------------------------------------------------------------- /bin/speedup_mp3: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "pp" 4 | require "shellwords" 5 | require "pathname" 6 | require "fileutils" 7 | 8 | class Pathname 9 | def shellescape 10 | to_s.shellescape 11 | end 12 | end 13 | 14 | class SpeedUpMp3 15 | def initialize(factor) 16 | @factor = factor 17 | end 18 | 19 | def known_tags 20 | %W[TPE1 TIT2 TALB TCON TIT3 TRCK TYER COMM] 21 | end 22 | 23 | def read_id3_tags_raw(source) 24 | # Most ID3 tags in the wild are not UTF-8 25 | `id3v2 -l #{source.to_s.shellescape}`.force_encoding("ISO-8859-1") 26 | end 27 | 28 | def read_id3_tags(source) 29 | read_id3_tags_raw(source).scan(/^([a-zA-Z0-9]{4}) \(.*?\): (.*)$/).select{|tag, value| 30 | known_tags.include?(tag) 31 | } 32 | end 33 | 34 | def copy_id3_tags!(source, target) 35 | tags = read_id3_tags(source).map{|tag, value| ["--#{tag}", value]}.flatten 36 | return if tags.empty? 37 | cmd = ["id3v2"] 38 | cmd += tags 39 | cmd += [target.to_s] 40 | system *cmd 41 | end 42 | 43 | def copy_timestamp!(source, target) 44 | FileUtils.touch(target, mtime: source.mtime) 45 | end 46 | 47 | def sox_m4a!(source, target) 48 | part_target = "#{target}.part" 49 | system %Q[ffmpeg -loglevel panic -i #{source.shellescape} -f wav pipe: | sox -t wav - -t mp3 #{part_target.shellescape} tempo -s #{@factor}] or raise "#{source} conversion failed" 50 | copy_timestamp!(source, part_target) 51 | system *%W[mv #{part_target} #{target}] 52 | end 53 | 54 | def sox!(source, target) 55 | part_target = "#{target}.part" 56 | system *%W[sox #{source} -t mp3 #{part_target} tempo -s #{@factor}] or raise "#{source} conversion failed" 57 | copy_timestamp!(source, part_target) 58 | system *%W[mv #{part_target} #{target}] 59 | end 60 | 61 | def speedup_video!(source, target) 62 | # TODO: there are ways to stack filters for other speeds 63 | raise "ffmpeg only supports speeds from 0.5 to 2.0" unless (0.5..2.0).include?(@factor) 64 | filter = "[0:v]setpts=PTS/#{@factor}[v];[0:a]atempo=#{@factor}[a]" 65 | part_target = target.dirname + "part-#{target.basename}" 66 | system *%W[ffmpeg -i #{source} -filter_complex #{filter} -map [v] -map [a] #{part_target}] 67 | copy_timestamp!(source, part_target) 68 | system *%W[mv #{part_target} #{target}] 69 | end 70 | 71 | def speedup_file!(source, target) 72 | target.dirname.mkpath 73 | return if source.basename.to_s == ".DS_Store" # OSX junk 74 | case source.extname.downcase 75 | when ".mp3" 76 | sox!(source, target) and copy_id3_tags!(source, target) 77 | when ".m4a", ".m4b", ".opus", ".ogg", ".aac" 78 | sox_m4a!(source, target.to_s.sub(/\.(?:m4a|m4b|mp4|opus|ogg)\z/i, ".mp3")) 79 | when ".mp4", ".mkv", ".m4v" 80 | speedup_video!(source, target) 81 | when ".wav" 82 | sox!(source, target.to_s.sub(/\.(?:wav)\z/i, ".mp3")) 83 | else 84 | warn "Don't know how to convert '#{source}' to '#{target}'" 85 | end 86 | end 87 | 88 | def speedup_to_directory!(source, target) 89 | if Pathname(source).directory? 90 | source.children.each do |child| 91 | target_child = target+child.basename 92 | if child.directory? 93 | speedup_to_directory!(child, target_child) 94 | else 95 | speedup_file!(child, target_child) 96 | end 97 | end 98 | else 99 | speedup_file!(source, target+source.basename) 100 | end 101 | end 102 | 103 | def run!(args) 104 | target = args.pop 105 | sources = args 106 | # TODO: Support autocreating target directory if it doesn't exist, 107 | # and better error messages? 108 | if target.directory? 109 | sources.each do |source| 110 | speedup_to_directory!(source, target) 111 | end 112 | elsif sources.size == 1 113 | source = sources[0] 114 | if source.directory? 115 | source.find do |source_path| 116 | target_path = target + source_path.relative_path_from(source) 117 | if source_path.directory? 118 | target_path.mkpath 119 | else 120 | speedup_file!(source_path, target_path) 121 | end 122 | end 123 | else 124 | speedup_file!(source, target) 125 | end 126 | else 127 | STDERR.puts "#{target} must be a directory to speedup multiple source files to it" 128 | exit 1 129 | end 130 | end 131 | end 132 | 133 | speed = 1.4 134 | 135 | if ARGV[0] =~ /\A-([\d\.]+)\z/ 136 | speed = $1.to_f 137 | ARGV.shift 138 | end 139 | 140 | if ARGV.empty? 141 | STDERR.puts "Usage: #{$0} [-factor] file_in.mp3 file_out.mp3" 142 | STDERR.puts " #{$0} [-factor] file1.mp3 file2.mp3 dir" 143 | STDERR.puts " #{$0} [-factor] dir_in dir_out" 144 | STDERR.puts "Default factor is 1.4 (40% faster)" 145 | exit 1 146 | end 147 | 148 | SpeedUpMp3.new(speed).run!(ARGV.map{|x| Pathname(x)}) 149 | -------------------------------------------------------------------------------- /bin/split_dir: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "pathname" 4 | 5 | class Array 6 | def balanced_slices(max_sz) 7 | slice_count = (size+max_sz-1) / max_sz 8 | (0...slice_count).map do |i| 9 | i0 = i * size / slice_count 10 | i1 = (i+1) * size / slice_count 11 | self[i0...i1] 12 | end 13 | end 14 | end 15 | 16 | max_size = 200 17 | 18 | ARGV.each do |d| 19 | d = Pathname.new(d) 20 | next unless d.directory? 21 | cn = d.children 22 | next if cn.size <= max_size * 1.25 # avoid tiny chunks 23 | # puts "Splitting #{d} - #{cn.size} files" 24 | slices = cn.sort.balanced_slices(max_size) 25 | slices.each_with_index do |fs, i| 26 | dn = d.sub(/\/?\Z/, "-%0#{slices.size.to_s.size}d" % (i+1)) 27 | if dn.exist? 28 | unless dn.directory? and dn.children.size == 0 29 | puts "#{dn} already exists and is not an empty directory, skipping" 30 | next 31 | end 32 | else 33 | dn.mkdir 34 | end 35 | fs.each do |f| 36 | nf = dn + f.basename 37 | if nf.exist? 38 | puts "#{nf} already exists, skipping" 39 | next 40 | end 41 | f.rename(nf) 42 | end 43 | end 44 | d.rmdir 45 | end 46 | -------------------------------------------------------------------------------- /bin/sqlite2json: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "pathname" 4 | require "sqlite3" 5 | require "json" 6 | 7 | # There are some stupid rounding issues with this 8 | class Sqlite2JSON 9 | def initialize(db_path, output_path) 10 | @db_path = Pathname(db_path) 11 | @output_path = Pathname(output_path) 12 | @db = SQLite3::Database.open(db_path) 13 | raise "Output directory already exists" if @output_path.exist? 14 | end 15 | 16 | def tables 17 | @tables ||= @db.execute("SELECT name FROM sqlite_master WHERE type='table'").flatten.sort 18 | end 19 | 20 | def run! 21 | @output_path.mkpath 22 | tables.each do |table| 23 | query = @db.prepare("SELECT * FROM `#{table}`") 24 | columns = query.columns 25 | data_json = query.execute.map{|data| columns.zip(data).to_h }.to_json 26 | (@output_path+"#{table}.json").open("w") do |fh| 27 | fh.puts data_json 28 | end 29 | end 30 | end 31 | end 32 | 33 | unless ARGV.size == 2 34 | STDERR.puts "Usage: #{$0} file.sqlite output_dir/" 35 | exit 1 36 | end 37 | 38 | Sqlite2JSON.new(*ARGV).run! 39 | -------------------------------------------------------------------------------- /bin/swap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "pathname" 4 | require "fileutils" 5 | 6 | def swap(*paths) 7 | paths.each do |path| 8 | unless Pathname(path).exist? 9 | STDERR.puts "#{path} does not exist, swap aborted" 10 | exit 1 11 | end 12 | end 13 | 14 | random_path = "tmp-#{rand(2**256).to_s(16)}" 15 | if Pathname(random_path).exist? 16 | STDERR.puts "#{random_path} exists, swap aborted" 17 | exit 1 18 | end 19 | 20 | [random_path, *paths, random_path].each_cons(2).reverse_each do |a,b| 21 | FileUtils.mv a, b 22 | end 23 | end 24 | 25 | 26 | unless ARGV.size >= 2 27 | STDERR.puts "Usage: #{$0} file1 file2 [file3 ...]" 28 | exit 1 29 | end 30 | 31 | swap(*ARGV) 32 | -------------------------------------------------------------------------------- /bin/tac: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | trap("PIPE", "EXIT") 4 | puts STDIN.read.split(/\r?\n/).reverse 5 | -------------------------------------------------------------------------------- /bin/terminal_title: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "color" 4 | 5 | def set_tab_background(color_name) 6 | color = Color::CSS[color_name] 7 | print "\e]6;1;bg;red;brightness;#{color.red.round}\a" 8 | print "\e]6;1;bg;green;brightness;#{color.green.round}\a" 9 | print "\e]6;1;bg;blue;brightness;#{color.blue.round}\a" 10 | end 11 | 12 | if ARGV[0] == "-c" or ARGV[0] == "--color" 13 | ARGV.shift 14 | set_tab_background(ARGV.shift) 15 | elsif ARGV[0] =~ /\A--(.*)/ 16 | ARGV.shift 17 | set_tab_background($1) 18 | end 19 | 20 | print "\e]0;", ARGV.join(" "), "\007" 21 | -------------------------------------------------------------------------------- /bin/tfl_travel_time: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # $VERBOSE = nil 4 | 5 | require "cgi" 6 | require "uri" 7 | require "open-uri" 8 | require "json" 9 | 10 | def parse_time(time) 11 | m = 0 12 | t = time.dup 13 | t.sub!(/Total time:/, "") 14 | m += $1.to_i * 60 if t.sub!(/(\d+)hr/, "") 15 | m += $1.to_i if t.sub!(/(\d+)mins?/, "") 16 | t.gsub!(/\s|\u{A0}/, "") 17 | raise "Time parse error: #{time.inspect}" unless t.empty? 18 | m 19 | end 20 | 21 | # We actually need URI.escape here, with " " becoming %20 not + 22 | def tfl_uri(from, to) 23 | from_esc = CGI.escape(from).gsub("+", "%20") 24 | to_esc = CGI.escape(to).gsub("+", "%20") 25 | "https://api.tfl.gov.uk/Journey/JourneyResults/#{from_esc}/to/#{to_esc}" 26 | end 27 | 28 | def travel_time(from, to) 29 | uri = tfl_uri(from, to) 30 | data = JSON.parse(URI.open(uri).read) 31 | data["journeys"].map{|j| j["duration"]}.min 32 | end 33 | 34 | from, to = ARGV 35 | t = travel_time(from, to) 36 | if t 37 | puts t 38 | else 39 | system "open", tfl_uri(from, to) 40 | warn "Can't find travel time from `#{from}' to `#{to}'" 41 | end 42 | -------------------------------------------------------------------------------- /bin/toutf8: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "pry" 4 | 5 | class ToUTF8 6 | def valid_ascii? 7 | @data.force_encoding("ASCII").valid_encoding? 8 | end 9 | 10 | def valid_utf8? 11 | @data.force_encoding("UTF-8").valid_encoding? 12 | end 13 | 14 | def has_utf8_bom? 15 | @data[0,3].b == "\xEF\xBB\xBF".b 16 | end 17 | 18 | def has_utf16le_bom? 19 | @data[0,2].b == "\xFF\xFE".b 20 | end 21 | 22 | def has_utf16be_bom? 23 | @data[0,2].b == "\xFE\xFF".b 24 | end 25 | 26 | def strip_utf8_bom 27 | @data = @data[3..-1] 28 | end 29 | 30 | def strip_utf16_bom 31 | @data = @data[2..-1] 32 | end 33 | 34 | def guess_utf16le? 35 | @data.unpack("v*").sum < @data.unpack("n*").sum 36 | end 37 | 38 | def convert_utf16le 39 | @data = @data.force_encoding("UTF-16LE").encode("UTF-8") 40 | end 41 | 42 | def convert_utf16be 43 | @data = @data.force_encoding("UTF-16BE").encode("UTF-8") 44 | end 45 | 46 | def call 47 | @data = ARGF.read.b 48 | if valid_ascii? 49 | # we're done 50 | elsif has_utf8_bom? 51 | strip_utf8_bom 52 | elsif valid_utf8? 53 | # we're done 54 | elsif has_utf16le_bom? 55 | strip_utf16_bom 56 | convert_utf16le 57 | elsif has_utf16be_bom? 58 | strip_utf16_bom 59 | convert_utf16be 60 | # Asssume it's some kind of UTF-16, which is honestly a bad assumption 61 | # but it's good enough v1 62 | elsif guess_utf16le? 63 | convert_utf16le 64 | else 65 | convert_utf16be 66 | end 67 | 68 | print @data 69 | end 70 | end 71 | 72 | ToUTF8.new.call 73 | -------------------------------------------------------------------------------- /bin/trash_size: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | system "du -hs ~/.Trash/" 4 | -------------------------------------------------------------------------------- /bin/unall: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "fileutils" 4 | require "shellwords" 5 | require "optimist" 6 | 7 | class UnarchiveFile 8 | Formats = { 9 | :rar => %w[.rar .cbr], 10 | :"7z" => %w[.7z .zip .cbz .jar .civ5mod], 11 | :tgz => %w[.tgz .tar.gz .gem], 12 | :tbz2 => %w[.tbz2 .tar.bz2], 13 | :tar => %w[.tar], 14 | :txz => %w[.tar.xz], 15 | :single_file => %w[.gz .bz2 .xz], 16 | } 17 | 18 | def formats 19 | @formats ||= Formats.map{|fmt, exts| exts.map{|ext| [fmt, ext]}}.flatten(1) 20 | end 21 | 22 | def initialize(path, force_separate_dir) 23 | @path = File.expand_path(path) 24 | @force_separate_dir = force_separate_dir 25 | end 26 | 27 | def mime_type 28 | `file -b --mime-type #{@path.shellescape}`.chomp 29 | end 30 | 31 | def basename 32 | File.basename(@path) 33 | end 34 | 35 | def call 36 | fmt_ext = detect_format or return "Not supported" 37 | fmt, ext = fmt_ext 38 | if needs_directory?(fmt) 39 | dnx = create_directory(basename[0...-ext.size]) 40 | Dir.chdir(dnx){ send("unpack_#{fmt}") ? "OK" : "FAIL" } 41 | else 42 | send("unpack_#{fmt}") ? "OK" : "FAIL" 43 | end 44 | end 45 | 46 | def create_directory(dn) 47 | counter = 1 48 | dnx = dn 49 | while File.exist?(dnx) 50 | dnx = "#{dn}-#{counter}" 51 | counter += 1 52 | end 53 | FileUtils.mkdir_p dnx 54 | return dnx 55 | end 56 | 57 | def needs_directory?(fmt) 58 | return true if @force_separate_dir 59 | prefixes = send("files_#{fmt}").map{|f| f.sub(/\/.*/, "")}.uniq.select{|f| f != ""} 60 | return true if prefixes.size > 1 61 | return true if File.exist?(prefixes[0]) 62 | false 63 | end 64 | 65 | def detect_format 66 | formats.each do |fmt, ext| 67 | if basename.downcase[-ext.size..-1] == ext 68 | return [fmt, ext] 69 | end 70 | end 71 | if mime_type == "application/zip" 72 | return [:"7z", File.extname(@path)] 73 | end 74 | return nil 75 | end 76 | 77 | def files_rar 78 | `unrar vb #{@path.shellescape}`.split("\n") 79 | end 80 | def files_7z 81 | # First is archive name 82 | `7za l -slt #{@path.shellescape}`.scan(/^Path = (.*)/).flatten[1..-1] 83 | end 84 | def files_tgz 85 | `tar -tzf #{@path.shellescape}`.split("\n") 86 | end 87 | def files_tbz2 88 | `tar -tjf #{@path.shellescape}`.split("\n") 89 | end 90 | def files_tar 91 | `tar -tf #{@path.shellescape}`.split("\n") 92 | end 93 | def files_txz 94 | `tar -tf #{@path.shellescape}`.split("\n") 95 | end 96 | def files_single_file 97 | [File.basename(@path, File.extname(@path))] 98 | end 99 | 100 | def unpack_rar 101 | system "unrar", "x", @path 102 | end 103 | def unpack_7z 104 | system "7za", "x", @path 105 | end 106 | def unpack_tgz 107 | system "tar", "-xzf", @path 108 | end 109 | def unpack_tbz2 110 | system "tar", "-xjf", @path 111 | end 112 | def unpack_txz 113 | system "tar", "-xf", @path 114 | end 115 | def unpack_tar 116 | system "tar", "-xf", @path 117 | end 118 | def unpack_single_file 119 | system "7za", "x", @path 120 | end 121 | end 122 | 123 | class UnarchiveCommand 124 | def initialize 125 | @opts = Optimist::options do 126 | opt :keep, "Keep original archive even if unpacking was successful" 127 | opt :dir, "Force unpacking into new directory even when all files are in one directory already" 128 | end 129 | 130 | if ARGV.empty? 131 | STDERR.puts "Usage:\n #{$0} [--keep] [--dir] archive1.zip archive2.rar archive3.7z" 132 | exit 1 133 | end 134 | @paths = ARGV 135 | end 136 | 137 | def call 138 | statuses = Hash.new{|ht,k| ht[k] = []} 139 | 140 | @paths.each do |path| 141 | ua = UnarchiveFile.new(path, @opts[:dir]) 142 | status = ua.call 143 | statuses[status] << path 144 | end 145 | 146 | statuses.each do |status, files| 147 | puts [status, *files].join(" ") 148 | system "trash", *files if status == "OK" and not @opts[:keep] 149 | end 150 | end 151 | end 152 | 153 | UnarchiveCommand.new.call 154 | -------------------------------------------------------------------------------- /bin/volume: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "shellwords" 4 | 5 | def osa(cmd) 6 | cmd = %W[osascript -e #{cmd}].map(&:shellescape).join(" ") 7 | `#{cmd}` 8 | end 9 | 10 | def get_volume 11 | osa("output volume of (get volume settings)") 12 | end 13 | 14 | def set_volume!(volume) 15 | osa("set volume output volume #{volume}") 16 | end 17 | 18 | case ARGV.size 19 | when 0 20 | puts get_volume 21 | when 1 22 | set_volume! ARGV[0].to_f 23 | else 24 | STDERR.puts "Usage: #{$0} volume # set new volume (0 to 100)" 25 | STDERR.puts " #{$0} # print current volume" 26 | exit 1 27 | end 28 | -------------------------------------------------------------------------------- /bin/webman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "digest" 4 | require "pathname" 5 | require "timeout" 6 | require "uri" 7 | require "fileutils" 8 | require "shellwords" 9 | 10 | module URI 11 | def self.for_form(base, args={}) 12 | uri = URI.parse(base) 13 | unless args.empty? 14 | # uri.query = URI.encode_www_form(args) # 1.9 only 15 | uri.query = args.map{|k,v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}"}.join("&") 16 | end 17 | uri 18 | end 19 | end 20 | 21 | class Pathname 22 | def nonempty_file? 23 | exist? and not zero? 24 | end 25 | 26 | # Removing Pathname#to_str in 1.9 was dumb, bringing it back 27 | alias_method :to_str, :to_s 28 | def shellescape 29 | to_s.shellescape 30 | end 31 | end 32 | 33 | def with_terminal_title(title) 34 | STDOUT.sync = true 35 | begin 36 | print "\e]0;Manual page for #{title}\007" 37 | yield 38 | ensure 39 | print "\e]0;Terminal\007" 40 | end 41 | end 42 | 43 | class ManPage 44 | attr_reader :cmd 45 | def initialize(cmd) 46 | @cmd = cmd 47 | end 48 | def cache_key 49 | Digest::MD5.hexdigest(cmd.inspect) 50 | end 51 | def cache_path 52 | Pathname("#{MAN_CACHE}/#{cache_key}") 53 | end 54 | def html_path 55 | Pathname("#{MAN_CACHE}/#{cache_key}.html") 56 | end 57 | def term_title 58 | "Manual page for #{cmd*' '}" 59 | end 60 | def man_path 61 | @man_path ||= `man -w #{cmd*' '} 2>/dev/null`.chomp 62 | end 63 | def ensure_fetched 64 | unless cache_path.nonempty_file? 65 | raise "File doesn't exist" unless File.exist?(man_path) 66 | if man_path =~ /\.(Z|gz)\z/i 67 | system "zcat <#{man_path.shellescape} >#{cache_path.shellescape}" 68 | else 69 | system "cat <#{man_path.shellescape} >#{cache_path.shellescape}" 70 | end 71 | end 72 | raise "Failed to fetch #{cmd*' '} to #{cache_path}" unless cache_path.nonempty_file? 73 | end 74 | def ensure_html 75 | ensure_fetched 76 | system "groff -P -I#{cache_path.shellescape}_ -mandoc #{cache_path.shellescape} -T html >#{html_path.shellescape} 2>/dev/null" unless html_path.nonempty_file? 77 | raise "Rendering failed" unless html_path.nonempty_file? 78 | end 79 | def display_terminal 80 | ensure_fetched 81 | with_terminal_title(term_title) do 82 | system "man", cache_path 83 | end 84 | end 85 | def display_browser 86 | begin 87 | ensure_html 88 | system "open", html_path 89 | rescue 90 | system "open", google_url.to_s 91 | end 92 | end 93 | def google_url 94 | URI.for_form("http://www.google.com/search", :q => ["man", *@cmd]*" ") 95 | end 96 | end 97 | 98 | MAN_CACHE = ENV["HOME"] + "/.man_cache" 99 | FileUtils.mkdir_p MAN_CACHE 100 | 101 | browser = true 102 | if ARGV[0] =~ /\A(?:(-T|--term)|(-))\z/ 103 | if $1 104 | browser = false 105 | else 106 | raise "Unrecognized option: #{ARGV[0]}" 107 | end 108 | ARGV.shift 109 | end 110 | 111 | if browser 112 | ManPage.new(ARGV).display_browser 113 | else 114 | ManPage.new(ARGV).display_terminal 115 | end 116 | -------------------------------------------------------------------------------- /bin/xmlview: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "shellwords" 4 | 5 | if ARGV.empty? 6 | exec "xmlindent -i 1 | colcut 150 | less" 7 | else 8 | exec "xmlindent -i 1 < #{ARGV[0].shellescape} | colcut 150 | less" 9 | end 10 | -------------------------------------------------------------------------------- /bin/xpstree: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ############### 4 | # Support for older versions of Ruby 5 | unless [].respond_to?(:group_by) 6 | class Array 7 | def group_by 8 | hsh = {} 9 | each do |element| 10 | key = yield(element) 11 | hsh[key] ||= [] 12 | hsh[key] << element 13 | end 14 | hsh 15 | end 16 | end 17 | end 18 | 19 | unless :x.respond_to?(:to_proc) 20 | class Symbol 21 | def to_proc 22 | proc{|obj, *args| obj.send(self, *args)} 23 | end 24 | end 25 | end 26 | 27 | ############### 28 | # Core extensions 29 | class Class 30 | def cached(*vars) 31 | vars.each do |var| 32 | ivar, no_cache = :"@#{var}", :"#{var}_without_cache" 33 | alias_method(no_cache, var) 34 | define_method(var) do 35 | instance_variable_set(ivar, send(no_cache)) unless instance_variable_get(ivar) 36 | instance_variable_get(ivar) 37 | end 38 | end 39 | end 40 | end 41 | 42 | class Array 43 | def map_with_first_last 44 | rv = [] 45 | each_with_index do |item, i| 46 | rv << yield(item, i==0, i==size-1) 47 | end 48 | rv 49 | end 50 | end 51 | 52 | class String 53 | def with_asides(asides) 54 | asides.empty? ? self : "#{self}(#{asides.join(",")})" 55 | end 56 | def highlight 57 | "\033[1;37m\033[44m" + self + "\033[0m" 58 | end 59 | def width 60 | gsub(/\033\[[0-9;]+m/, "").size 61 | end 62 | end 63 | 64 | ############### 65 | # Boxes 66 | class Box 67 | attr_reader :key, :lines 68 | def initialize(key, lines) 69 | @lines = lines 70 | @key = key 71 | end 72 | def map(&blk) 73 | Box.new(key, lines.map(&blk)) 74 | end 75 | def box 76 | self 77 | end 78 | def to_s 79 | @lines.map{|x| x+"\n"}.join 80 | end 81 | def max_line_width 82 | lines.map(&:width).max 83 | end 84 | cached :max_line_width 85 | def pad 86 | map do |line| 87 | line + (" " * (max_line_width-line.width)) 88 | end 89 | end 90 | def append(str) 91 | pad.map do |line| 92 | line + str 93 | end 94 | end 95 | def prepend(*prefixes) 96 | map do |line| 97 | prefixes.push prefixes.last 98 | prefixes.shift + line 99 | end 100 | end 101 | def extended_lines(first_box, last_box) 102 | if first_box and last_box 103 | prepend("--", " ") 104 | elsif last_box 105 | prepend("`-", " ") 106 | elsif first_box 107 | prepend("+-", "| ") 108 | else 109 | prepend("|-", "| ") 110 | end 111 | end 112 | def title_line(title) 113 | prepend(title, " " * title.width) 114 | end 115 | def multiply(n) 116 | return self if n == 1 117 | pad.map{|line| "[#{line}]"}.title_line("#{n}*") 118 | end 119 | def self.merge_boxes(objects) 120 | objects.map(&:box).group_by(&:lines).map{|lines, boxes| 121 | boxes.sort_by(&:key).first.multiply(boxes.size) 122 | }.sort_by(&:key) 123 | end 124 | def self.parent_with_children(key, parent_line, children, below) 125 | lines = merge_boxes(children).map_with_first_last{|cb, is_first, is_last| 126 | cb.extended_lines(is_first&&!below, is_last).lines 127 | }.flatten 128 | if lines.empty? or below 129 | Box.new(key, [parent_line, *lines]).prepend("", " ") 130 | else 131 | Box.new(key, lines).title_line("#{parent_line}-") 132 | end 133 | end 134 | end 135 | 136 | ############### 137 | # Trees 138 | module Tree 139 | attr_reader :parent 140 | def parent=(new_parent) 141 | @parent.children.delete(self) if @parent 142 | @parent = new_parent 143 | new_parent.children << self if new_parent 144 | end 145 | def children 146 | @children ||= [] 147 | end 148 | # Can be used even in operations that modify @children 149 | def each_child(&blk) 150 | children.dup.each(&blk) 151 | end 152 | def prune!(&blk) 153 | if parent and yield(self) 154 | self.parent = nil 155 | else 156 | each_child{|c| c.prune!(&blk)} 157 | end 158 | end 159 | def skip_self! 160 | each_child{|c| c.parent = parent} 161 | self.parent = nil 162 | end 163 | def skip!(&blk) 164 | each_child{|c| c.skip!(&blk)} 165 | skip_self! if parent and yield(self) 166 | end 167 | def focus!(&blk) 168 | out_of_focus = parent && !yield(self) 169 | each_child{|c| c.focus!(&blk)} if out_of_focus or not parent 170 | skip_self! if out_of_focus 171 | end 172 | def highlight!(&blk) 173 | each_child{|c| c.highlight!(&blk) } 174 | end 175 | def each(&blk) 176 | yield(self) 177 | each_child{|c| c.each(&blk)} 178 | end 179 | end 180 | 181 | class TreeCollection 182 | include Tree 183 | def initialize(nodes) 184 | tree = Hash.new(self) 185 | nodes.each{|key, pkey, node| tree[key] = node} 186 | nodes.each{|key, pkey, node| tree[key].parent = tree[pkey]} 187 | end 188 | def to_s 189 | Box.merge_boxes(children).join 190 | end 191 | end 192 | 193 | ############### 194 | # Horrible horrible mess which should die in fire 195 | 196 | def Process.command_name(command) 197 | return $1 if command =~ %r[\A\[(\S+?)(?:/\d+)?\]\Z] 198 | w0, w1, = *command.split(/\s+/).map{|cmd| File.basename(cmd).sub(/:\Z/, "") } 199 | return w1 if w0 =~ /\A(perl|ruby|sh|bash)\Z/ and w1 and w1 !~ /\A-/ 200 | return w0.sub(/\A-/, "") 201 | end 202 | 203 | def Process.list 204 | `ps -eo pid,ppid,uid,user,command`.split(/\n/)[1..-1].map{|line| 205 | pid, ppid, uid, user, command = *line.chomp.split(' ', 5) 206 | [pid.to_i, ppid.to_i, ProcessTree.new(pid.to_i, uid.to_i, user, command)] 207 | } 208 | end 209 | 210 | class ProcessTree 211 | include Tree 212 | attr_reader :pid, :uid, :user, :command, :highlighted 213 | attr_accessor :raw_command_lines 214 | def initialize(pid, uid, user, command) 215 | @pid, @uid, @user, @command = pid, uid, user, command 216 | @highlighted = false 217 | @raw_command_lines = false 218 | end 219 | def parent_process 220 | parent.is_a?(ProcessTree) ? parent : nil 221 | end 222 | def set_asides(display_pids) 223 | @asides = [ 224 | display_pids ? pid : nil, 225 | parent_process && user != parent_process.user ? user : nil, 226 | ].compact 227 | end 228 | def command_name 229 | Process.command_name(command) 230 | end 231 | def pretty_name 232 | pn = (raw_command_lines ? command : command_name).with_asides(@asides) 233 | highlighted ? pn.highlight : pn 234 | end 235 | def box 236 | Box.parent_with_children(pid, pretty_name, children, raw_command_lines) 237 | end 238 | def to_s 239 | box.to_s 240 | end 241 | def inspect 242 | ppid = parent_process ? parent_process.pid : nil 243 | cpids = children.map(&:pid).join(",") 244 | "Process[#{command_name};#{pid};#{user};#{ppid};#{cpids};#{command}]" 245 | end 246 | def highlight!(&blk) 247 | super 248 | @highlighted = children.any?(&:highlighted) || yield(self) 249 | end 250 | # Caching command_name and box may not be such a brilliant idea, 251 | # as parent and children can change, and this cache never gets invalidated 252 | cached :box, :pretty_name, :command_name 253 | end 254 | 255 | def mkfilter(rule) 256 | proc{|x| rule === x.command_name} 257 | end 258 | 259 | display_pids = false 260 | raw_command_lines = false 261 | filters = [[:prune!, mkfilter('kthread')]] 262 | while arg = ARGV.shift 263 | case arg 264 | when "--help" 265 | puts " 266 | Options: 267 | -u focus on current process 268 | -s highlight current process 269 | -p print PIDs 270 | -h name highlight processes named name 271 | -H rx highlight processes matching /rx/i 272 | -f name focus on processed named name 273 | -F rx focus on processes matching /rx/i 274 | -x name prune processes named name 275 | -X rx prune processes matching /rx/i 276 | -S rx skip processes matching /rx/ 277 | --raw print full command line arguments 278 | --help print this message 279 | " 280 | exit 281 | when '-u' 282 | filters << [:focus!, proc{|x| x.uid == Process.uid }] 283 | when '-s' 284 | filters << [:highlight!, proc{|x| x.pid == Process.pid}] 285 | when '--raw' 286 | raw_command_lines = true 287 | when '-p' 288 | display_pids = true 289 | when /\A-([hHxsfXSF])\z/ 290 | actions = {'h' => :highlight!, 'x' => :prune!, 's' => :skip!, 'f' => :focus!} 291 | type = $1 292 | pattern = ARGV.shift 293 | if type.downcase == type 294 | pattern = /\A#{Regexp.quote(pattern)}\z/i 295 | else 296 | pattern = /#{pattern}/i 297 | end 298 | filters << [actions[type.downcase], mkfilter(pattern)] 299 | else 300 | warn "Unknown option, ignoring: #{arg}" 301 | end 302 | end 303 | 304 | roots = TreeCollection.new(Process.list) 305 | filters.each{|type,arg| roots.send(type,&arg)} 306 | roots.each do |n| 307 | next unless n.is_a?(ProcessTree) 308 | n.raw_command_lines = raw_command_lines 309 | n.set_asides(display_pids) 310 | end 311 | puts roots 312 | -------------------------------------------------------------------------------- /bin/xrmdir: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "fileutils" 4 | require "pathname" 5 | require "optimist" 6 | 7 | def xrmdir(path) 8 | return if path.symlink? 9 | return unless path.directory? 10 | 11 | ds_store = path + ".DS_Store" 12 | if ds_store.exist? 13 | ds_store.unlink 14 | puts "Removing #{ds_store}" if @opts[:verbose] 15 | end 16 | 17 | if @opts[:recursive] 18 | path.children.each do |child| 19 | xrmdir child 20 | end 21 | end 22 | 23 | if path.children.empty? 24 | puts "Removing empty directory #{path}" if @opts[:verbose] 25 | begin 26 | path.rmdir 27 | rescue 28 | warn "Can't remove directory #{path}: #{$!}" 29 | end 30 | end 31 | end 32 | 33 | @opts = Optimist::options do 34 | opt :verbose, "Be verbose" 35 | opt :recursive, "Remove empty subdirectories first" 36 | end 37 | 38 | ARGV.each do |dir| 39 | xrmdir Pathname(dir) 40 | end 41 | -------------------------------------------------------------------------------- /spec/colcut_spec.rb: -------------------------------------------------------------------------------- 1 | describe "colcut" do 2 | let(:binary) { Pathname(__dir__)+"../bin/colcut" } 3 | it "cuts columns to 120 characters by default" do 4 | IO.popen("#{binary}", "r+") do |fh| 5 | fh.puts "x" * 200 6 | fh.close_write 7 | expect(fh.read).to eq( 8 | "x" * 120 + "\n" 9 | ) 10 | end 11 | end 12 | 13 | it "cuts columns to specified number of characters" do 14 | IO.popen("#{binary} 20", "r+") do |fh| 15 | fh.print "abc\n" 16 | fh.print ("a".."z").to_a.join + "\n" 17 | fh.print "abcde\n" 18 | fh.close_write 19 | expect(fh.read).to eq( 20 | "abc\n" + 21 | "abcdefghijklmnopqrst\n" + 22 | "abcde\n" 23 | ) 24 | end 25 | end 26 | 27 | it "forces standard line endings" do 28 | IO.popen("#{binary}", "r+") do |fh| 29 | fh.print "x\r\n" 30 | fh.print "y\n" 31 | fh.print "z" 32 | 33 | fh.close_write 34 | expect(fh.read).to eq( 35 | "x\ny\nz\n" 36 | ) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/countdown_spec.rb: -------------------------------------------------------------------------------- 1 | describe "countdown" do 2 | let(:binary) { Pathname(__dir__)+"../bin/countdown" } 3 | 4 | it "counts down, then runs specified command" do 5 | IO.popen("#{binary} 5 echo Done", "r+") do |fh| 6 | expect(fh.read).to eq([ 7 | "\r0:05", 8 | "\r0:04", 9 | "\r0:03", 10 | "\r0:02", 11 | "\r0:01", 12 | "\r0:00", 13 | "\rSTART!\n", 14 | "Done\n", 15 | ].join) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/coverage_spec.rb: -------------------------------------------------------------------------------- 1 | describe "coverage" do 2 | (Pathname(__dir__) + "../bin").children.each do |path| 3 | if (Pathname(__dir__) + "#{path.basename}_spec.rb").exist? 4 | # OK 5 | else 6 | pending "#{path.basename} has some tests" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fix_permissions_spec.rb: -------------------------------------------------------------------------------- 1 | describe "fix_permissions" do 2 | let(:binary) { Pathname(__dir__)+"../bin/fix_permissions" } 3 | 4 | it "normalizes file names" do 5 | MockUnix.new do |env| 6 | File.write("foo.sh", "#!/bin/bash\necho 123") 7 | File.write("foo.rb", "#!/bin/env ruby") 8 | File.write("hello.txt", "Hello, world!") 9 | File.write("true", File.read(`which true`.chomp)) 10 | File.write("empty.txt", "") 11 | system "chmod +x *" 12 | system "#{binary} *" 13 | 14 | actual = env.path 15 | .find 16 | .map{|x| [x.relative_path_from(env.path).to_s, x.stat.mode & 0o777] } 17 | .select{|x,| x != "."} 18 | .sort 19 | 20 | expect(actual).to eq([ 21 | ["empty.txt", 0o666 &~ File.umask], 22 | ["foo.rb", 0o777 &~ File.umask], 23 | ["foo.sh", 0o777 &~ File.umask], 24 | ["hello.txt", 0o666 &~ File.umask], 25 | ["true", 0o777 &~ File.umask], 26 | ]) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/flickr_find_spec.rb: -------------------------------------------------------------------------------- 1 | describe "flickr_find" do 2 | let(:binary) { Pathname(__dir__)+"../bin/flickr_find" } 3 | 4 | it "opens browser with proper search" do 5 | MockUnix.new do |env| 6 | env.mock_command "open" 7 | env.mock_command "xdg-open" 8 | system "'#{binary}' 'kitten'" 9 | open_trace = env.command_trace("xdg-open") + env.command_trace("open") 10 | expect(open_trace).to eq([ 11 | ["https://www.flickr.com/search/?text=kitten&license=2%2C3%2C4%2C5%2C6%2C9"], 12 | ]) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/gzip_stream_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe "gzip_stream" do 3 | # To verify that plain gzip doesn't work for it: 4 | # let(:binary) { "gzip" } 5 | let(:binary) { Pathname(__dir__)+"../bin/gzip_stream" } 6 | it "gzips" do 7 | IO.popen("#{binary} | zcat", "r+") do |fh| 8 | fh.puts *(1..1000) 9 | fh.close_write 10 | expect(fh.readlines).to eq((1..1000).map{|x| "#{x}\n"}) 11 | end 12 | end 13 | 14 | it "doesn't wait for end of input" do 15 | IO.popen("#{binary}", "r+") do |fh| 16 | fh.puts *(1..1000) 17 | sleep 2 18 | a = fh.read_nonblock(10_000) 19 | fh.close_write 20 | b = fh.read 21 | # Data 22 | expect(a.size).to be_between(1750, 1900) 23 | # Just finalization mark 24 | expect(b.size).to eq(9) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/json_pp_spec.rb: -------------------------------------------------------------------------------- 1 | describe "json_pp" do 2 | let(:binary) { Pathname(__dir__)+"../bin/json_pp" } 3 | 4 | it "prettyprints json" do 5 | IO.popen("#{binary}", "r+") do |fh| 6 | fh.puts '{"a":[1,2,3],"b":"foo","c":{"d":"e"}}' 7 | fh.close_write 8 | expect(fh.read).to eq( 9 | '{ 10 | "a": [ 11 | 1, 12 | 2, 13 | 3 14 | ], 15 | "b": "foo", 16 | "c": { 17 | "d": "e" 18 | } 19 | } 20 | ') 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/lastfm_status_spec.rb: -------------------------------------------------------------------------------- 1 | describe "lastfm_status" do 2 | let(:binary) { Pathname(__dir__)+"../bin/lastfm_status" } 3 | 4 | # Something old and abandoned or close enough 5 | # timezone last.fm returns doesn't seem to be consistent 6 | it "real account" do 7 | expect(`#{binary} niphree`).to match( 8 | /\Aniphree's last song was `Master of Tides' by `Lindsey Stirling' at 2015-05-13 \d+:45:00 \+\d{4}\nIt was \d+d \d+h \d+m\d+s ago\n\z/ 9 | ) 10 | end 11 | 12 | it "bad account" do 13 | expect(`#{binary} no_such_username`).to eq("No recent songs by no_such_username\n") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/namenorm_spec.rb: -------------------------------------------------------------------------------- 1 | describe "namenorm" do 2 | let(:binary) { Pathname(__dir__)+"../bin/namenorm" } 3 | 4 | it "normalizes file names" do 5 | MockUnix.new do |env| 6 | FileUtils.touch "KATY PERRY - ROAR.MP3" 7 | FileUtils.touch "ubuntu.14.04.iso.gz" 8 | FileUtils.touch "INDEX.HTM" 9 | FileUtils.touch "read me.txt" 10 | system "#{binary} *" 11 | expect(env).to have_content([ 12 | "index.htm", 13 | "katy_perry_-_roar.mp3", 14 | "read_me.txt", 15 | "ubuntu.14.04.iso.gz", 16 | ]) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/open_youtube_spec.rb: -------------------------------------------------------------------------------- 1 | describe "open_youtube" do 2 | let(:binary) { Pathname(__dir__)+"../bin/open_youtube" } 3 | 4 | it "opens URLs based on ID in file names passed" do 5 | MockUnix.new do |env| 6 | env.mock_command "open" 7 | system "#{binary}", "FOAR EVERYWUN FRUM BOXXY-Yavx9yxTrsw.mkv" 8 | expect(env.command_trace("open")).to eq([ 9 | ["https://www.youtube.com/watch?v=Yavx9yxTrsw"], 10 | ]) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/openmany_spec.rb: -------------------------------------------------------------------------------- 1 | describe "openmany" do 2 | let(:binary) { Pathname(__dir__)+"../bin/openmany" } 3 | 4 | it "opens files listed on command line" do 5 | MockUnix.new do |env| 6 | env.mock_command "open" 7 | env.mock_command "xdg-open" 8 | system "#{binary} a b c" 9 | open_trace = env.command_trace("xdg-open") + env.command_trace("open") 10 | expect(open_trace).to eq([ 11 | ["a"], 12 | ["b"], 13 | ["c"], 14 | ]) 15 | end 16 | end 17 | 18 | it "opens files listed from STDIN if no command line arguments are passed" do 19 | MockUnix.new do |env| 20 | env.mock_command "open" 21 | env.mock_command "xdg-open" 22 | IO.popen("#{binary}", "r+") do |fh| 23 | fh.puts "d" 24 | fh.puts "e" 25 | fh.puts "f" 26 | fh.close_write 27 | end 28 | open_trace = env.command_trace("xdg-open") + env.command_trace("open") 29 | expect(open_trace).to eq([ 30 | ["d"], 31 | ["e"], 32 | ["f"], 33 | ]) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/rand_passwd_spec.rb: -------------------------------------------------------------------------------- 1 | describe "since_soup" do 2 | let(:binary) { Pathname(__dir__)+"../bin/rand_passwd" } 3 | 4 | it "generates random different strings" do 5 | passwords = 10.times.map{`#{binary}`.chomp} 6 | expect(passwords.size).to eq(passwords.uniq.size) 7 | passwords.each do |password| 8 | expect(password).to match(/\A[a-z]{12}\z/) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/randsample_spec.rb: -------------------------------------------------------------------------------- 1 | describe "randsample" do 2 | let(:binary) { Pathname(__dir__)+"../bin/randsample" } 3 | 4 | it "gets one sample if no arguments passed" do 5 | results = 10.times.map{`seq 100 | #{binary}`} 6 | expect(results.map(&:lines).map(&:size)).to eq([1] * 10) 7 | end 8 | 9 | it "gets N samples if argument passed" do 10 | results = 10.times.map{`seq 100 | #{binary} 7`} 11 | expect(results.map(&:lines).map(&:size)).to eq([7] * 10) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/randswap_spec.rb: -------------------------------------------------------------------------------- 1 | describe "randswap" do 2 | let(:binary) { Pathname(__dir__)+"../bin/randswap" } 3 | 4 | it "generates random permutations" do 5 | permutations = 10.times.map{`seq 100 | #{binary}`} 6 | expect(permutations.size).to eq(permutations.uniq.size) 7 | permutations.each do |permutation| 8 | expect(permutation.lines.sort_by(&:to_i)).to eq (1..100).map{|i| "#{i}\n"} 9 | end 10 | end 11 | 12 | it "normalizes newlines" do 13 | IO.popen("#{binary}", "r+") do |fh| 14 | fh.print "a\r\nb\nc" 15 | fh.close_write 16 | expect(fh.read.lines.sort).to eq(["a\n", "b\n", "c\n"]) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/rbexe_spec.rb: -------------------------------------------------------------------------------- 1 | shared_examples "rbexe" do 2 | it do 3 | MockUnix.new do |env| 4 | system "#{binary}", *arguments, "#{file}" 5 | expect(file).to be_executable 6 | expect(content).to eq("#!#{interpretter}\n") 7 | end 8 | end 9 | end 10 | 11 | describe "rbexe" do 12 | let(:binary) { Pathname(__dir__)+"../bin/rbexe" } 13 | let(:file) { Pathname("test") } 14 | let(:content) { file.read } 15 | 16 | context "default" do 17 | let(:arguments) { %W[] } 18 | let(:interpretter) { "/usr/bin/env ruby" } 19 | it_behaves_like "rbexe" 20 | end 21 | 22 | context "ruby" do 23 | let(:arguments) { %W[--rb] } 24 | let(:interpretter) { "/usr/bin/env ruby" } 25 | it_behaves_like "rbexe" 26 | end 27 | 28 | context "perl" do 29 | let(:arguments) { %W[--pl] } 30 | let(:interpretter) { "/usr/bin/perl" } 31 | it_behaves_like "rbexe" 32 | end 33 | 34 | # This means python 2 on pretty much every system out there 35 | context "python" do 36 | let(:arguments) { %W[--py] } 37 | let(:interpretter) { "/usr/bin/env python" } 38 | it_behaves_like "rbexe" 39 | end 40 | 41 | context "python 2" do 42 | let(:arguments) { %W[--py2] } 43 | let(:interpretter) { "/usr/bin/env python2" } 44 | it_behaves_like "rbexe" 45 | end 46 | 47 | context "python 3" do 48 | let(:arguments) { %W[--py3] } 49 | let(:interpretter) { "/usr/bin/env python3" } 50 | it_behaves_like "rbexe" 51 | end 52 | 53 | context "bash" do 54 | let(:arguments) { %W[--sh] } 55 | let(:interpretter) { "/bin/bash" } 56 | it_behaves_like "rbexe" 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/rename_spec.rb: -------------------------------------------------------------------------------- 1 | describe "rename" do 2 | let(:binary) { Pathname(__dir__)+"../bin/rename" } 3 | 4 | it "renames files based on perl script passed as argument" do 5 | MockUnix.new do |env| 6 | FileUtils.touch "one.txt" 7 | FileUtils.touch "two.txt" 8 | FileUtils.touch "three.md" 9 | FileUtils.touch "tfour.txt" 10 | system "'#{binary}' 's/txt/html/' t*" 11 | expect(env).to have_content([ 12 | "one.txt", 13 | "tfour.html", 14 | "three.md", 15 | "two.html", 16 | ]) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/rjq_spec.rb: -------------------------------------------------------------------------------- 1 | describe "rjq" do 2 | let(:binary) { Pathname(__dir__)+"../bin/rjq" } 3 | 4 | it "rjq process and pretty-print" do 5 | IO.popen("#{binary} '$_=$_.map(&:abs)'", "r+") do |fh| 6 | fh.puts "[-1,2,-3,4]" 7 | fh.close_write 8 | expect(fh.read).to eq( 9 | "[\n"+ 10 | " 1,\n"+ 11 | " 2,\n"+ 12 | " 3,\n"+ 13 | " 4\n"+ 14 | "]\n" 15 | ) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/rot13_spec.rb: -------------------------------------------------------------------------------- 1 | describe "rot13" do 2 | let(:binary) { Pathname(__dir__)+"../bin/rot13" } 3 | 4 | it "reads from STDIN if used without arguments" do 5 | IO.popen("#{binary}", "r+") do |fh| 6 | fh.print "Lorem ipsum dolor sit amet, consectetur adipisicing elit\n" 7 | fh.close_write 8 | expect(fh.read).to eq("Yberz vcfhz qbybe fvg nzrg, pbafrpgrghe nqvcvfvpvat ryvg\n") 9 | end 10 | end 11 | 12 | it "doesn't affect end of lines" do 13 | IO.popen("#{binary}", "r+") do |fh| 14 | fh.print "a\r\nb\nc" 15 | fh.close_write 16 | expect(fh.read).to eq("n\r\no\np") 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/sortby_spec.rb: -------------------------------------------------------------------------------- 1 | describe "sortby" do 2 | let(:binary) { Pathname(__dir__)+"../bin/sortby" } 3 | 4 | it "sortby abs" do 5 | IO.popen("#{binary} '$_.to_i.abs'", "r+") do |fh| 6 | fh.puts "12" 7 | fh.puts "-91" 8 | fh.puts "34" 9 | fh.puts "-17" 10 | fh.close_write 11 | expect(fh.read).to eq( 12 | "12\n"+ 13 | "-17\n"+ 14 | "34\n"+ 15 | "-91\n" 16 | ) 17 | end 18 | end 19 | 20 | it "sortby size" do 21 | IO.popen("#{binary} '[$_.size, $_]'", "r+") do |fh| 22 | fh.puts %W[Lorem ipsum dolor sit amet, consectetur adipisicing elit] 23 | fh.close_write 24 | expect(fh.read.tr("\n", " ")).to eq( 25 | "sit elit Lorem amet, dolor ipsum adipisicing consectetur " 26 | ) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "pry" 2 | require "pathname" 3 | require "tmpdir" 4 | require "fileutils" 5 | require "timeout" 6 | 7 | class MockUnix 8 | attr_reader :path 9 | def initialize 10 | Dir.mktmpdir do |root_dir| 11 | Dir.mktmpdir do |bin_dir| 12 | @bin_path = Pathname(bin_dir) 13 | @path = Pathname(root_dir) 14 | @saved_env_path = ENV["PATH"] 15 | Dir.chdir(@path) do 16 | begin 17 | ENV["PATH"] = "#{@bin_path}:#{@saved_env_path}" 18 | yield(self) 19 | ensure 20 | ENV["PATH"] = @saved_env_path 21 | end 22 | end 23 | end 24 | end 25 | end 26 | 27 | def mock_command(name) 28 | cmd_path = @bin_path+name 29 | cmd_path.open("w", 0755) do |fh| 30 | fh.puts "#!/usr/bin/env ruby" 31 | fh.puts "open(#{ command_trace_path('open').to_s.inspect }, 'a'){|fh| fh.puts ARGV.inspect}" 32 | end 33 | end 34 | 35 | def command_trace_path(name) 36 | Pathname((@bin_path+name).to_s+".trace") 37 | end 38 | 39 | def command_trace(name) 40 | path = command_trace_path(name) 41 | if path.exist? 42 | path.readlines.map{|line| eval(line)} 43 | else 44 | [] 45 | end 46 | end 47 | end 48 | 49 | RSpec::Matchers.define :have_content do |expected| 50 | match do |env| 51 | actual = env.path.find.map{|x| x.relative_path_from(env.path) }.select{|x| x != Pathname(".")}.map(&:to_s) 52 | expected.sort == actual.sort 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/split_dir_spec.rb: -------------------------------------------------------------------------------- 1 | describe "split_dir" do 2 | let(:binary) { Pathname(__dir__)+"../bin/split_dir" } 3 | 4 | it "splits directories with a lot of files" do 5 | MockUnix.new do |path| 6 | FileUtils.mkdir_p "test" 7 | (1..1207).each do |i| 8 | FileUtils.touch("test/%04d.txt" % i) 9 | end 10 | system "#{binary} test" 11 | expect(path).to have_content([ 12 | "test-1", 13 | "test-2", 14 | "test-3", 15 | "test-4", 16 | "test-5", 17 | "test-6", 18 | "test-7", 19 | ( 1.. 172).map{|i| "test-1/%04d.txt" % i }, 20 | ( 173.. 344).map{|i| "test-2/%04d.txt" % i }, 21 | ( 345.. 517).map{|i| "test-3/%04d.txt" % i }, 22 | ( 518.. 689).map{|i| "test-4/%04d.txt" % i }, 23 | ( 690.. 862).map{|i| "test-5/%04d.txt" % i }, 24 | ( 863..1034).map{|i| "test-6/%04d.txt" % i }, 25 | (1035..1207).map{|i| "test-7/%04d.txt" % i }, 26 | ].flatten) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/swap_spec.rb: -------------------------------------------------------------------------------- 1 | describe "swap" do 2 | let(:binary) { Pathname(__dir__)+"../bin/swap" } 3 | 4 | it "swaps 2 files" do 5 | MockUnix.new do |env| 6 | Pathname("one.txt").write("1") 7 | Pathname("two.txt").write("2") 8 | system "#{binary}", "one.txt", "two.txt" 9 | expect(env).to have_content([ 10 | "one.txt", 11 | "two.txt", 12 | ]) 13 | expect(Pathname("one.txt").read).to eq("2") 14 | expect(Pathname("two.txt").read).to eq("1") 15 | end 16 | end 17 | 18 | it "swaps more than 2 files" do 19 | MockUnix.new do |env| 20 | Pathname("one.txt").write("1") 21 | Pathname("two.txt").write("2") 22 | Pathname("three.txt").write("3") 23 | Pathname("four.txt").write("4") 24 | system "#{binary}", "one.txt", "two.txt", "three.txt", "four.txt" 25 | expect(env).to have_content([ 26 | "one.txt", 27 | "two.txt", 28 | "three.txt", 29 | "four.txt", 30 | ]) 31 | expect(Pathname("one.txt").read).to eq("4") 32 | expect(Pathname("two.txt").read).to eq("1") 33 | expect(Pathname("three.txt").read).to eq("2") 34 | expect(Pathname("four.txt").read).to eq("3") 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/tac_spec.rb: -------------------------------------------------------------------------------- 1 | describe "tac" do 2 | let(:binary) { Pathname(__dir__)+"../bin/tac" } 3 | 4 | it "prints lines in reverse" do 5 | IO.popen("#{binary}", "r+") do |fh| 6 | fh.print "a\nb\nc\n" 7 | fh.close_write 8 | expect(fh.read).to eq("c\nb\na\n") 9 | end 10 | end 11 | 12 | it "normalizes newlines" do 13 | IO.popen("#{binary}", "r+") do |fh| 14 | fh.print "a\r\nb\nc" 15 | fh.close_write 16 | expect(fh.read).to eq("c\nb\na\n") 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/terminal_title_spec.rb: -------------------------------------------------------------------------------- 1 | describe "terminal_title" do 2 | let(:binary) { Pathname(__dir__)+"../bin/terminal_title" } 3 | 4 | it "prints sequence to set terminal title" do 5 | expect(`#{binary} test`).to eq("\e]0;test\a") 6 | end 7 | 8 | it "handles colors by their CSS name" do 9 | expect(`#{binary} -c pink test`).to eq( 10 | "\e]6;1;bg;red;brightness;255\a"+ 11 | "\e]6;1;bg;green;brightness;192\a"+ 12 | "\e]6;1;bg;blue;brightness;203\a"+ 13 | "\e]0;test\a" 14 | ) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/tfl_travel_time_spec.rb: -------------------------------------------------------------------------------- 1 | describe "tfl_travel_time" do 2 | let(:binary) { Pathname(__dir__)+"../bin/tfl_travel_time" } 3 | let(:angel) { "EC1V 1NE" } 4 | let(:waterstones) { "W1J 9HD" } 5 | 6 | it "can tell travel time" do 7 | travel_time = `"#{binary}" "#{angel}" "#{waterstones}"` 8 | expect(travel_time).to match(/\A\d{2,3}\n\z/) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/toutf8/ascii.txt: -------------------------------------------------------------------------------- 1 | All your base are belong to us. 2 | -------------------------------------------------------------------------------- /spec/toutf8/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taw/unix-utilities/595be73e5a74ad8fc4702b29453ae2e2bebd47e7/spec/toutf8/empty.txt -------------------------------------------------------------------------------- /spec/toutf8/utf16be.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taw/unix-utilities/595be73e5a74ad8fc4702b29453ae2e2bebd47e7/spec/toutf8/utf16be.txt -------------------------------------------------------------------------------- /spec/toutf8/utf16be_bom.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taw/unix-utilities/595be73e5a74ad8fc4702b29453ae2e2bebd47e7/spec/toutf8/utf16be_bom.txt -------------------------------------------------------------------------------- /spec/toutf8/utf16le.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taw/unix-utilities/595be73e5a74ad8fc4702b29453ae2e2bebd47e7/spec/toutf8/utf16le.txt -------------------------------------------------------------------------------- /spec/toutf8/utf16le_bom.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taw/unix-utilities/595be73e5a74ad8fc4702b29453ae2e2bebd47e7/spec/toutf8/utf16le_bom.txt -------------------------------------------------------------------------------- /spec/toutf8/utf8.txt: -------------------------------------------------------------------------------- 1 | Żółw błotny. 2 | -------------------------------------------------------------------------------- /spec/toutf8/utf8_bom.txt: -------------------------------------------------------------------------------- 1 | Żółw błotny. 2 | -------------------------------------------------------------------------------- /spec/toutf8_spec.rb: -------------------------------------------------------------------------------- 1 | describe "toutf8" do 2 | let(:binary) { Pathname(__dir__)+"../bin/toutf8" } 3 | let(:path) { Pathname(__dir__) + "toutf8/#{file}.txt" } 4 | let(:output) { `#{binary} <#{path.to_s.shellescape}` } 5 | 6 | context "empty" do 7 | let(:file) { "empty" } 8 | it "leave is as is" do 9 | expect(output).to eq("") 10 | end 11 | end 12 | 13 | context "ascii" do 14 | let(:file) { "ascii" } 15 | it "" do 16 | expect(output).to eq("All your base are belong to us.\n") 17 | end 18 | end 19 | 20 | context "UTF-8" do 21 | let(:file) { "utf8" } 22 | it "" do 23 | expect(output).to eq("Żółw błotny.\n") 24 | end 25 | end 26 | 27 | context "UTF-8 with BOM" do 28 | let(:file) { "utf8_bom" } 29 | it "converts to UTF-8 without BOM" do 30 | expect(output).to eq("Żółw błotny.\n") 31 | end 32 | end 33 | 34 | context "UTF-16-BE" do 35 | let(:file) { "utf16be" } 36 | it "converts to UTF-8 without BOM" do 37 | expect(output).to eq("Żółw błotny.\n") 38 | end 39 | end 40 | 41 | context "UTF-16-BE with BOM" do 42 | let(:file) { "utf16be_bom" } 43 | it "converts to UTF-8 without BOM" do 44 | expect(output).to eq("Żółw błotny.\n") 45 | end 46 | end 47 | 48 | context "UTF-16-LE" do 49 | let(:file) { "utf16le" } 50 | it "converts to UTF-8 without BOM" do 51 | expect(output).to eq("Żółw błotny.\n") 52 | end 53 | end 54 | 55 | context "UTF-16-LE with BOM" do 56 | let(:file) { "utf16le_bom" } 57 | it "converts to UTF-8 without BOM" do 58 | expect(output).to eq("Żółw błotny.\n") 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/unall_spec.rb: -------------------------------------------------------------------------------- 1 | describe "unall" do 2 | let(:binary) { Pathname(__dir__)+"../bin/unall" } 3 | 4 | def create_archive 5 | File.write("a.txt", "hello") 6 | File.write("b.txt", "world") 7 | yield 8 | FileUtils.remove "a.txt" 9 | FileUtils.remove "b.txt" 10 | end 11 | 12 | def unpacks_and_deletes_archive(binary, name) 13 | system %Q["#{binary}" "#{name}" >/dev/null] 14 | files = Pathname(".").find.select(&:file?).reject{|n| n.to_s =~ /.DS_Store/}.map{|n| [n.to_s, n.read]} 15 | expect(files).to eq([["foo/a.txt", "hello"], ["foo/b.txt", "world"]]) 16 | end 17 | 18 | %W[7z zip tar].each do |format| 19 | it "unzips archives in #{format} format" do 20 | MockUnix.new do |env| 21 | create_archive do 22 | system "7za a foo.#{format} a.txt b.txt >/dev/null" 23 | end 24 | unpacks_and_deletes_archive binary, "foo.#{format}" 25 | end 26 | end 27 | end 28 | 29 | it "unzips archives in tar.gz format" do 30 | MockUnix.new do |env| 31 | create_archive do 32 | system "tar c a.txt b.txt | gzip >foo.tar.gz" 33 | end 34 | unpacks_and_deletes_archive binary, "foo.tar.gz" 35 | end 36 | end 37 | 38 | it "unzips archives in tar.bz2 format" do 39 | MockUnix.new do |env| 40 | create_archive do 41 | system "tar c a.txt b.txt | bzip2 >foo.tar.bz2" 42 | end 43 | unpacks_and_deletes_archive binary, "foo.tar.bz2" 44 | end 45 | end 46 | 47 | it "unzips archives in .zip format even with nonstandard extension" do 48 | MockUnix.new do |env| 49 | create_archive do 50 | system "7za a foo.zip a.txt b.txt >/dev/null" 51 | system "mv foo.zip foo.wtf" 52 | end 53 | unpacks_and_deletes_archive binary, "foo.wtf" 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/xrmdir_spec.rb: -------------------------------------------------------------------------------- 1 | describe "xrmdir" do 2 | let(:binary) { Pathname(__dir__)+"../bin/xrmdir" } 3 | 4 | it "removes empty directories, killing .DS_Store on the way" do 5 | MockUnix.new do |env| 6 | FileUtils.mkdir_p "one" 7 | FileUtils.mkdir_p "two" 8 | FileUtils.mkdir_p "three/four" 9 | FileUtils.mkdir_p "three/five" 10 | FileUtils.touch "two/.DS_Store" 11 | FileUtils.touch "three/five/.DS_Store" 12 | system "#{binary} *" 13 | expect(env).to have_content([ 14 | "three", 15 | "three/five", 16 | "three/five/.DS_Store", 17 | "three/four", 18 | ]) 19 | end 20 | end 21 | 22 | it "recursively removes empty directories, killing .DS_Store on the way" do 23 | MockUnix.new do |env| 24 | FileUtils.mkdir_p "one" 25 | FileUtils.mkdir_p "two" 26 | FileUtils.mkdir_p "three/four" 27 | FileUtils.mkdir_p "three/five" 28 | FileUtils.mkdir_p "six/seven" 29 | FileUtils.mkdir_p "six/eight" 30 | FileUtils.touch "two/.DS_Store" 31 | FileUtils.touch "three/five/.DS_Store" 32 | FileUtils.touch "six/eight/nine.txt" 33 | system "#{binary} -r *" 34 | expect(env).to have_content([ 35 | "six", 36 | "six/eight", 37 | "six/eight/nine.txt", 38 | ]) 39 | end 40 | end 41 | end 42 | --------------------------------------------------------------------------------