#!/usr/bin/env ruby # -*- coding: utf-8 -*- ### ### gr8 -- a great command-line utility powered by Ruby ### ### $Release: 0.1.1 $ ### $Copyright: copyright(c) 2015 kuwata-lab.com all rights reserved $ ### $License: MIT License $ ### require "optparse" def fu #; [!ktccp] returns FileUtils class object. require "fileutils" unless defined?(FileUtils) FileUtils end class String def q #; [!ejo5y] quotes string with single-quoation. #; [!ageyj] escapes single-quotation characters. "'%s'" % self.gsub(/'/) { "\\'" } end def qq #; [!wwvll] quotes string with double-quotation. #; [!rc66j] escapes double-quotation characters. '"%s"' % self.gsub(/"/) { '\\"' } end end class Object ## [Experimental] ## With Object#_, you can use '_' character as each value ## in block argument of map() or select(). ## For example, 'map{|s|s+".bkp"}' can be 'map{_+".bkp"}'. ## See 'transform()', 'map()' or 'select()' for details. def _ #; [!wvemx] returns self object. self end end module Enumerable def transform(&block) #; [!peitw] similar to map() or collect(), make each item as self in block. collect {|x| x.instance_exec(x, &block) } end alias xf transform alias __map map def map(&block) #; [!zfmcx] each item is available as self in block of map(). __map {|x| x.instance_exec(x, &block) } end alias __select select def select(&block) #; [!41hap] each item is available as self in block of select(). __select {|x| x.instance_exec(x, &block) } end def sum #; [!9izc1] returns sum of numbers. inject(0, :+) end def sum_i #; [!01ehd] returns sum of integers, converting values into integer. inject(0) {|t, x| t + x.to_i } end def sum_f #; [!kplnt] returns sum of floats, converting values into float. inject(0.0) {|t, x| t + x.to_f } end def avg #; [!pvi8h] returnns average of numbers. #; [!poidi] returns nil when no numbers. i = 0 sum = inject(0) {|t, n| i += 1; t + n } i == 0 ? nil : sum.to_f / i end def avg_i #; [!btiat] returns average of numbers, converting values into integer. #; [!892q9] returns nil when no numbers. i = 0 sum = inject(0) {|t, x| i += 1; t + x.to_i } i == 0 ? nil : sum.to_f / i end def avg_f #; [!oqpmc] returns average of numbers, converting values into float. #; [!9bckq] returns nil when no numbers. i = 0 sum = inject(0) {|t, x| i += 1; t + x.to_f } i == 0 ? nil : sum.to_f / i end def xsplit(pat=nil) #; [!1pz77] splits each lines with pattern. #; [!wte7b] if block given, use its result as index. if block_given? idx = yield collect {|s| s.split(pat)[idx] } else collect {|s| s.split(pat) } end end def sed(pat, str=nil, &block) #; [!c7m34] replaces all patterns found in each line with str or block. if block_given? collect {|s| s.sub(pat, &block) } else collect {|s| s.sub(pat, str) } end end def gsed(pat, str=nil, &block) #; [!9lzjv] replaces first pattern found in each line with str or block. if block_given? collect {|s| s.gsub(pat, &block) } else collect {|s| s.gsub(pat, str) } end end def paths(&block) #; [!t55ce] collects Pathname objects when block argument is not passed. #; [!yjkm5] yields Pathname objects when block argument is passed. #; [!4kppy] self is Patname object in block argument. require "pathname" unless defined?(Pathname) if block_given? collect {|s| x = Pathname(s); x.instance_exec(x, &block) } else collect {|s| Pathname(s) } end end def edit(verbose=true, encoding='utf-8', &block) edit_i(nil, verbose, encoding, &block) end def edit_i(suffix, verbose=true, encoding='utf-8', &block) require "fileutils" unless defined?(FileUtils) arity = block.arity collect {|fpath| fpath.strip! msg = nil if File.file?(fpath) #; [!lpncu] creates backup file with suffix spedified. if suffix && ! suffix.empty? bkup_fpath = "#{fpath}#{suffix}" FileUtils.mv(fpath, bkup_fpath) FileUtils.cp(bkup_fpath, fpath) end #; [!ur9mj] opens file with utf-8 encoding. File.open(fpath, 'r+', encoding: encoding) do |f| s = f.read() s1 = s + "" s1.object_id != s.object_id or raise "** assertion failed" #; [!qqegl] file content and file path are passed to block argument. #; [!d8dxv] make content as self in block argument. s2 = s.instance_exec(s, fpath, &block) #; [!9g7re] edit file when content changed. if s1 != s2 f.rewind() f.truncate(0) f.write(s2.to_s) msg = "Edit: '#{fpath}'" if verbose #; [!exzkz] don't edit file when content not changed. else msg = "NotChanged: '#{fpath}'" if verbose end end else #; [!k9d31] skips if file not exist. #; [!6m49n] skips if file is not a file. if ! File.exist?(fpath) msg = "Skip: '#{fpath}' does not exist." else msg = "Skip: '#{fpath}' is not a file." end end msg }.reject {|x| x.nil? } end ## experimentals def move_to(verbose=true, &block) #:experimental: __copy_or_move_to("Move", verbose, false, false, "move_to", &block) end def move_to!(verbose=true, &block) #:experimental: __copy_or_move_to("Move", verbose, true, false, "move_to!", &block) end def mkdir_and_move_to(verbose=true, &block) #:experimental: __copy_or_move_to("Move", verbose, false, true, "mkdir_and_move_to", &block) end def mkdir_and_move_to!(verbose=true, &block) #:experimental: __copy_or_move_to("Move", verbose, true, true, "mkdir_and_move_to!", &block) end def copy_to(verbose=true, &block) #:experimental: __copy_or_move_to("Copy", verbose, false, false, "copy_to", &block) end def copy_to!(verbose=true, &block) #:experimental: __copy_or_move_to("Copy", verbose, true, false, "copy_to!", &block) end def mkdir_and_copy_to(verbose=true, &block) #:experimental: __copy_or_move_to("Copy", verbose, false, true, "mkdir_and_copy_to", &block) end def mkdir_and_copy_to!(verbose=true, &block) #:experimental: __copy_or_move_to("Copy", verbose, true, true, "mkdir_and_copy_to!", &block) end def rename_as(verbose=true, &block) #:experimental: __copy_or_rename_as("Rename", verbose, false, false, "rename_as", &block) end def rename_as!(verbose=true, &block) #:experimental: __copy_or_rename_as("Rename", verbose, true, false, "rename_as!", &block) end def mkdir_and_rename_as(verbose=true, &block) #:experimental: __copy_or_rename_as("Rename", verbose, false, true, "mkdir_and_rename_as", &block) end def mkdir_and_rename_as!(verbose=true, &block) #:experimental: __copy_or_rename_as("Rename", verbose, true, true, "mkdir_and_rename_as!", &block) end def copy_as(verbose=true, &block) #:experimental: __copy_or_rename_as("Copy", verbose, false, false, "copy_as", &block) end def copy_as!(verbose=true, &block) #:experimental: __copy_or_rename_as("Copy", verbose, true, false, "copy_as!", &block) end def mkdir_and_copy_as(verbose=true, &block) #:experimental: __copy_or_rename_as("Copy", verbose, false, true, "mkdir_and_copy_as", &block) end def mkdir_and_copy_as!(verbose=true, &block) #:experimental: __copy_or_rename_as("Copy", verbose, true, true, "mkdir_and_copy_as!", &block) end private def __copy_or_move_to(action, verbose_p, overwrite_p, mkdir_p, meth, &block) #; [!n0ubo] block argument is required. #; [!40se5] block argument is required. #; [!k74dw] block argument is required. #; [!z9yus] block argument is required. block or raise ArgumentError.new("#{meth}(): block argument required.") require "fileutils" unless defined?(FileUtils) existing = nil collect {|fpath| #; [!qqzqz] trims target file name. fpath.strip! #; [!nnud9] destination directory name is derived from target file name. dirpath = fpath.instance_exec(fpath, &block) #; [!ey3e4] if target directory name is nil or empty, skip moving file. if ! dirpath || dirpath.empty? msg = "Skip: target directory name is nil or empty (file: '#{fpath}')" #; [!i5jt6] if destination directory exists, move file to it. elsif dirpath == existing || File.directory?(dirpath) msg = nil #; [!azqgk] if there is a file that name is same as desination directory, skip. elsif File.exist?(dirpath) msg = "Skip: directory '#{dirpath}' not a directory" #; [!b9d4m] if destination directory doesn't exist, creates it. elsif mkdir_p FileUtils.mkdir_p(dirpath) msg = nil #; [!rqu5q] if destinatio directory doesn't exist, skip. else msg = "Skip: directory '#{dirpath}' not exist" end # if msg.nil? new_fpath = File.join(dirpath, File.basename(fpath)) #; [!0gq9h] if destination file already exist, skip. exist_p = File.exist?(new_fpath) if exist_p && ! overwrite_p msg = "Skip: destination file '#{new_fpath}' already exist." #; [!ebdqh] overwrite destination file even if it exists. else #; [!fa5y0] copy files or directories into destination directory. if action == "Copy" FileUtils.cp_r(fpath, dirpath) #; [!d9vxl] move files or directories into destination directory. elsif action == "Move" FileUtils.mv(fpath, dirpath) else raise "** unreachable" end existing = dirpath #; [!n7a1q] prints target file and destination directory when verbose mode. #; [!itsh0] use 'Move!' instead of 'Move' when overwriting existing file. msg = "#{action}#{exist_p ? '!' : ''}: '#{fpath}' => '#{dirpath}'" if verbose_p end end msg }.reject {|s| s.nil? } end def __copy_or_rename_as(action, verbose_p, overwrite_p, mkdir_p, meth, &block) #; [!ignfm] block argument is required. block or raise ArgumentError.new("#{meth}(): block argument required.") require "fileutils" unless defined?(FileUtils) existing = nil collect {|fpath| #; [!qqzqz] trims target file name. fpath.strip! #; [!nnud9] destination file name is derived from source file name. new_fpath = fpath.instance_exec(fpath, &block) # overwritten = false #; [!dkejf] if target directory name is nil or empty, skips renaming file. if ! new_fpath || new_fpath.empty? msg = "Skip: target file name is nil or empty (file: '#{fpath}')" # elsif File.exist?(new_fpath) #; [!1yzjd] if target file or directory already exists, removes it before renaming file. if overwrite_p FileUtils.rm_rf(new_fpath) overwritten = true msg = nil #; [!8ap57] if target file or directory already exists, skips renaming files. else msg = "Skip: target file '#{new_fpath}' already exists." end # else #; [!qhlc8] if directory of target file already exists, renames file. dirpath = File.dirname(new_fpath) if existing == dirpath || File.directory?(dirpath) existing = dirpath msg = nil #; [!sh2ti] if directory of target file not exist, creates it. elsif mkdir_p FileUtils.mkdir_p(dirpath) existing = dirpath msg = nil #; [!gg9w1] if directory of target file not exist, skips renaming files. else msg = "Skip: directory of target file '#{new_fpath}' not exist." end end if msg.nil? #; [!0txp4] copy files or directories. if action == "Copy" FileUtils.cp_r(fpath, new_fpath) #; [!xi8u5] rename files or directories. elsif action == "Rename" FileUtils.move(fpath, new_fpath) else raise "** unreachable" end #; [!vt24y] prints source and destination file path when verbose mode. #; [!gd9j9] use 'Rename!' instead of 'Rename' when overwriting existing file. #; [!8warb] use 'Copy!' instead of 'Copy' when overwriting exsiting file. msg = "#{action}#{overwritten ? '!': ''}: '#{fpath}' => '#{new_fpath}'" if verbose_p end msg }.reject {|s| s.nil? } end end class Enumerator::Lazy alias __map map def map(&block) #; [!drgky] each item is available as self in block of map(). __map {|x| x.instance_exec(x, &block) } end alias __select select def select(&block) #; [!uhqz2] each item is available as self in block of map(). __select {|x| x.instance_exec(x, &block) } end end module Gr8 VERSION = "$Release: 0.1.1 $".split()[1] WEBSITE_URL = "https://kwatch.github.io/gr8/" HELP = <<"END" %{script} -- great command-line utility powered by Ruby Usage: %{script} [options] ruby-code Options: -h, --help : print help --doc : open document with browser -v, --version : print version -r lib[,lib2,...] : require libraries -F[regexp] : separate each line into fields -C N : select column (1-origin) Example: $ cat data Haruhi 100 Mikuru 80 Yuki 120 $ cat data | %{script} 'map{|s| s.split()[1]}' ## self == $stdin.lazy 100 80 120 $ cat data | %{script} 'map{split()[1].to_i}.sum' ## map{self} == map{|s| s} 300 $ cat data | %{script} 'map{split[1]}.sum_i' ## .sum_i == map(&:to_i).sum 300 $ cat data | %{script} -C2 'sum_i' ## -C2 == map{split[1]} 300 See #{WEBSITE_URL} for details and examples. END class EnumWrapper include Enumerable def initialize(enum, separator=nil, column=nil) @_base = enum @_separator = separator @_column = column end def each #; [!hloy1] splits each line into array. #; [!c22km] chomps each lin before splitting. #; [!m411f] selects column when column number specified. sep = @_separator col = @_column if @_column index = @_column - 1 @_base.each {|s| s.chomp!; yield s.split(sep)[index] } else @_base.each {|s| s.chomp!; yield s.split(sep) } end end end class App def run(*args) # begin opts = parse_options(args) rescue ::OptionParser::ParseError => ex #$stderr.puts "ERROR (#{script_name()}): #{ex.args.is_a?(Array) ? ex.args.join(' ') : ex.args}: #{ex.reason}" $stderr.puts "ERROR (#{script_name()}): #{ex.message}" return 1 end # output = handle_opts(opts) if output puts output return 0 end # errmsg = validate_args(args) if errmsg $stderr.puts "ERROR (#{script_name()}): #{errmsg}" return 1 end #; [!8hk3g] option '-F': separates each line into array. #; [!vnwu6] option '-C': select colum. if opts[:separator] || opts[:column] sep = opts[:separator] stdin = EnumWrapper.new($stdin, sep == true ? nil : sep, opts[:column]).lazy else stdin = $stdin.lazy define_singleton_methods_on(stdin) end #; [!r69d6] executes ruby code with $stdin.lazy as self. code = args[0] filename = "<#{script_name()}>" val = stdin.instance_eval(code, filename) #; [!hsvnd] prints nothing when result is nil. #; [!eiaa6] prints each item when result is Enumerable. #; [!6pfay] prints value when result is not nil nor Enumerable. case val when nil ; nil when Enumerable ; val.each {|x| puts x } else ; puts val end #; [!h5wln] returns 0 as status code when executed successfully. return 0 end def main(argv=ARGV) #; [!w9kb8] exit with status code 0 when executed successfully. #; [!nbag1] exit with status code 1 when execution failed. args = argv.dup status = run(*args) exit status end private def script_name @script_name ||= File.basename($0) end def parse_options(args) #; [!5efp5] returns Hash object containing command-line options. opts = {} parser = OptionParser.new parser.on("-h", "--help") {|v| opts[:help] = true } parser.on( "--doc") {|v| opts[:doc] = true } parser.on("-v", "--version") {|v| opts[:version] = true } parser.on("-r lib[,lib2,..]") {|v| opts[:require] = v } parser.on("-F[sep]") do |v| #; [!jt4y5] option '-F': separator is omissible. #; [!jo4gm] option '-F': error when invalid regular expression. begin opts[:separator] = v.nil? ? true : Regexp.new(v) rescue RegexpError raise invalid_argument_error(v, "invalid regular expression") end end #parser.on("-C N", Integer) {|v| opts[:column] = v } parser.on("-C N") do |v| #; [!7ruq0] option '-C': argument should be an integer. #; [!6x3dp] option '-C': argument should be >= 1. begin opts[:column] = Integer(v) rescue ArgumentError => ex raise invalid_argument_error(v, "integer expected") end if opts[:column] <= 0 raise invalid_argument_error(v, "column number should be >= 1") end end #; [!wdzss] modifies args. parser.parse!(args) return opts end def invalid_argument_error(optarg, reason) err = OptionParser::InvalidArgument.new(optarg) err.reason = reason err end def handle_opts(opts) #; [!33bj3] option '-h', '--help': prints help message. if opts[:help] return HELP % {script: script_name()} end #; [!7dvjg] option '--doc': opens website with browser. if opts[:doc] cmd = command_to_open_website() system cmd return cmd end #; [!2tfh5] option '-v', '--version': prints version string. if opts[:version] return VERSION end #; [!1s7wm] option '-r': requires libraries. if opts[:require] opts[:require].split(/,/).each do |libname| libname.strip! require libname end end nil end def command_to_open_website() url = WEBSITE_URL case RUBY_PLATFORM when /darwin/ ; "open #{url}" when /linux/ ; "xdg-open #{url}" when /bsd/ ; "xdg-open #{url}" # really? when /win|mingw/i ; "start #{url}" else ; "open #{url}" # or error? end end def validate_args(args) #; [!7wqyh] prints error when no argument. #; [!bwiqv] prints error when too many argument. if args.length == 0 return "argument required." elsif args.length > 1 return "too many arguments." end nil end def define_singleton_methods_on(stdin) (class << stdin; self; end).class_eval do #; [!zcxh1] removes '\n' from each line automatically. alias __each_orig each def each __each_orig {|s| s.chomp!; yield s } end alias __each_new each #; [!i7npb] $1, $2, ... are available in grep() block argument. #; [!vkt64] lines are chomped automatically in grep() if block is not given. def grep(pattern, &block) if pattern.is_a?(Regexp) && block (class << self; self; end).class_eval { alias each __each_orig } end super(pattern, &block) end end end end end #if __FILE__ == $0 Gr8::App.new.main() unless $DONT_RUN_GR8_APP #end