aboutsummaryrefslogtreecommitdiff
path: root/arr
diff options
context:
space:
mode:
Diffstat (limited to 'arr')
-rwxr-xr-xarr119
1 files changed, 119 insertions, 0 deletions
diff --git a/arr b/arr
new file mode 100755
index 0000000..8d5ba5a
--- /dev/null
+++ b/arr
@@ -0,0 +1,119 @@
+#!/usr/bin/env ruby
+# arr EXPR [FILES...] - (re)arrange and select fields on each line
+#
+# To the extent possible under law,
+# Christian Neukirchen <chneukirchen@gmail.com>
+# has waived all copyright and related or neighboring rights to this work.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+USAGE = <<'EOF'
+Usage: arr [-0] [-P|-p PADDING] EXPR [FILES...]
+ EXPR ::= FIELDS (("|" CHAR | "*") FIELDS)* # | split on char, * split bytes
+ FIELDS ::= "~"? FIELD ("," FIELD)* # ~ negates
+ FIELD ::= "-"? "\d"+ # negative fields count from back
+ | ("-"? "\d"+)? ":" ("-"? "\d"+)? # range ends default to 1:-1
+EOF
+
+require 'strscan'
+require 'optparse'
+
+def fmt(ss, d)
+ last_split = " "
+
+ loop {
+ fields = []
+ neg = false
+
+ begin
+ i = j = nil
+ neg = true if ss.scan(/~/)
+ if ss.scan(/:|-?\d+/)
+ i = Integer(ss.matched) rescue 1
+ i = i < 0 ? d.size + i : i - 1
+ if ss.matched == ":" || ss.scan(/:/)
+ if ss.scan(/-?\d+/)
+ j = Integer(ss.matched)
+ else
+ j = -1
+ end
+ j = j < 0 ? d.size + j : j - 1
+
+ if j > i
+ fields.concat (i..j).to_a
+ else
+ fields.concat (j..i).to_a.reverse
+ end
+ else
+ fields << i
+ end
+ else
+ abort "parse error at #{ss.inspect}"
+ end
+ end while ss.scan(/,/)
+
+ d = d.values_at(*if neg
+ (0..d.size).to_a - fields
+ else
+ fields
+ end)
+
+ if ss.scan(/\|(.)/)
+ d = d.join(last_split)
+ last_split = ss[1]
+ d = d.split(last_split)
+ elsif ss.scan(/\*/)
+ d = d.join(last_split)
+ last_split = ""
+ d = d.split('')
+ else
+ break
+ end
+ }
+
+ unless ss.scan(/\}/)
+ abort "parse error at #{ss.inspect}"
+ end
+
+ d.compact.join(last_split)
+end
+
+def fmt2(str, arr)
+ ss = StringScanner.new(str)
+
+ r = ""
+
+ while ss.scan(/(.*?)%\{/)
+ r << ss[1]
+ r << fmt(ss, arr)
+ end
+
+ r << ss.rest
+end
+
+begin
+ params = ARGV.getopts('0Pp:')
+ nl = params["0"] ? "\0" : "\n"
+ padding = params["p"] || ""
+ padding = nil if params["P"]
+
+ expr = ARGV.shift or raise OptionParser::MissingArgument, "no EXPR given"
+
+ ARGV << "-" if ARGV.empty?
+
+ files = ARGV.map { |name|
+ if name == "-"
+ STDIN
+ else
+ File.open(name, "rb")
+ end
+ }
+
+ until files.all?(&:eof?)
+ lines = files.map { |f| f.gets(nl).chomp(nl) rescue padding }
+ break if lines.include?(nil)
+ print fmt2(expr, lines), nl
+ end
+rescue OptionParser::ParseError
+ STDERR.puts $!
+ STDERR.puts USAGE
+end