Skip to content

Commit d27379a

Browse files
committed
Add the initial collection
1 parent e4703fa commit d27379a

File tree

7 files changed

+374
-3
lines changed

7 files changed

+374
-3
lines changed

README.md

+120-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,121 @@
1-
util
2-
====
1+
# Utils
32

4-
Little CLI tools that I find useful. YMMV.
3+
This is a small set of tools that scratch various personal itches. You
4+
may or may not find and of them remotely useful.
5+
6+
7+
(See also the excellent [moreutils][].)
8+
9+
[moreutils]: https://joeyh.name/code/moreutils/
10+
11+
## dl
12+
13+
`find`, `mdfind`, `grep -l`, and similar commands emit a succession
14+
of file paths, one per line. These are very easy for programs to
15+
parse. Unfortunately, they're a little harder for humans to read,
16+
especially when the question the humans are trying to answer is "Which
17+
directory should I go poking around in first?"
18+
19+
`dl takes a succession of paths as input, performs some grouping, and
20+
emits a set of per-directory listings.
21+
22+
As a simple example, `grep -rl ruby` in one of my personal directories
23+
generates the following output:
24+
25+
./artifacts/closest_polynomial
26+
./artifacts/envy
27+
./artifacts/fizzbuzz.rb
28+
./artifacts/rcd
29+
./artifacts/uri_descape
30+
./util/dl
31+
./util/jj
32+
./util/loo
33+
./util/path
34+
./wip/frag
35+
./wip/modulist
36+
./wip/nn
37+
38+
Piped through `dl`, this produces:
39+
40+
./artifacts
41+
closest_polynomial
42+
envy
43+
fizzbuzz.rb
44+
rcd
45+
uri_descape
46+
47+
./util
48+
dl
49+
jj
50+
loo
51+
path
52+
53+
./wip
54+
frag
55+
modulist
56+
nn
57+
58+
The optional `-d` argument goes one step further and omits mention of
59+
the files, listing only the directories.
60+
61+
## git-purge
62+
63+
This should really be fleshed out into a script that does some basic
64+
argument-checking. (In particular, if not passed a path, it will
65+
neither complain nor do anything of interest.)
66+
67+
It is nevertheless useful if you want to brutally and thoroughly clear
68+
a repository, or part thereof, of any local changes. *Nota bene* that
69+
it will **lose data**---indeed, that's the whole point---so use with
70+
care.
71+
72+
73+
## jj
74+
75+
This tool pretty-prints JSON. In this, it is much like the doubtless
76+
orders-of-magnitude faster binary [jsonpp][]. Unlike jsonpp, however,
77+
it does not expect/demand to be given input in the form of one object
78+
per unbroken line. (To put it another way: jsonpp chokes when fed
79+
properly- or even partially-formatted JSON, such as its own output,
80+
while jj does not.)
81+
82+
There are some minor differences in output style between jj and
83+
jsonpp. The former, for instance, always splits an array over multiple
84+
lines, even when the array in question is empty, whereas the latter
85+
prints empty arrays on a single line.
86+
87+
[jsonpp]: https://github.com/jmhodges/jsonpp
88+
89+
## loo
90+
91+
Short for **l**ast **o**ccurrence **o**nly. (Get your mind out of the
92+
gutter.) `loo` removes all but the last instance of any repeatedly-
93+
occurring lines from its input. Unlike `uniq`, it's not limited to
94+
repitions of successive lines.
95+
96+
Given the input:
97+
98+
Alice
99+
Bob
100+
Alice
101+
David
102+
Charles
103+
David
104+
105+
`loo` would emit:
106+
107+
Bob
108+
Alice
109+
Charles
110+
David
111+
112+
Since it would drop the first occurrences of 'Alice' and 'David'. If invoked as `foo` (via the magic of soft/hard links), it will instead produce output containing only the *first* instance of repeatedly-occurring lines. The above input would become:
113+
114+
Alice
115+
Bob
116+
David
117+
Charles
118+
119+
## path
120+
121+
At least 50 cents of solution to a nickel's worth of problem. So it goes. I hate deciphering the output of `echo $PATH`; now I don't have to. By default, this script pretty-prints the value of `$PATH`, with one entry per line. However, it can also be used to generate a new colon-separated value for `$PATH`, optionally removing duplicate occurrences of paths.

Rakefile

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
bin_dir = File.expand_path "~/bin"
2+
directory bin_dir
3+
4+
files = %w[ dl jj loo path ]
5+
files += Dir.glob("git-*")
6+
7+
task :default => :deploy
8+
9+
task :deploy => bin_dir do
10+
files.each do |file|
11+
target = "#{bin_dir}/#{file}"
12+
unless FileUtils.uptodate?(target, [file])
13+
# Setting the preserve flag preserves the exec bits, but also
14+
# preserves the original timestamp, which isn't really what we
15+
# want. We therefore invoke touch as a follow-up.
16+
FileUtils.cp(file, target, preserve: true)
17+
FileUtils.touch(target)
18+
puts "Updated #{target}."
19+
end
20+
end
21+
end

dl

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env ruby -w
2+
3+
# dl: directory list.
4+
#
5+
# Take a collection of paths from either stdin or a collection of files.
6+
#
7+
# Group items by their parent directories.
8+
9+
require 'optparse'
10+
11+
class PathSet
12+
def initialize
13+
@paths = Hash.new { |hash, key| hash[key] = [] }
14+
end
15+
16+
def add(path)
17+
@paths[File.dirname(path)] << File.basename(path)
18+
end
19+
20+
def show(dirs_only = false)
21+
@paths.sort.each do |dir, files|
22+
puts dir
23+
unless dirs_only
24+
files.each do |file|
25+
puts " #{file}"
26+
end
27+
puts
28+
end
29+
end
30+
end
31+
end
32+
33+
34+
def parse_args
35+
options = {}
36+
37+
op = OptionParser.new do |opts|
38+
opts.banner = "Usage: #{opts.program_name} [option]... [argument]..."
39+
40+
options[:dirs_only] = false
41+
opts.on("-d", "--dirs_only", "Show only the directory components.") do
42+
options[:dirs_only] = true
43+
end
44+
45+
end
46+
47+
op.parse!
48+
49+
options
50+
end
51+
52+
# -----------------
53+
54+
options = parse_args
55+
56+
pathset = PathSet.new()
57+
58+
ARGF.each_line do |line|
59+
pathset.add(line.chomp)
60+
end
61+
62+
pathset.show(options[:dirs_only])

git-purge

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env bash
2+
3+
git reset -q HEAD ${@}
4+
git checkout -- ${@}
5+
git clean -fd ${@}

jj

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env ruby -w
2+
3+
# Pretty-print JSON input from a file, or stdin.
4+
5+
require 'json'
6+
7+
# To do (maybe):
8+
# - Add an option to insert a splitting line between the prettifications of
9+
# multiple sources. (Easy.)
10+
# - Add an option to convert multiple sources into a single array.
11+
# (Trickier, but doable.)
12+
13+
input = String.new
14+
ARGF.each_line do |line|
15+
input << line
16+
if ARGF.eof?
17+
jj JSON.parse(input)
18+
input.clear
19+
end
20+
end

loo

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env ruby -w
2+
3+
# loo: last ocurrence only.
4+
#
5+
# Return the last occurrence of every line in the input.
6+
#
7+
# If called as 'foo' (via the magic of symlinks), outputs the FIRST
8+
# occurence of each line instead.
9+
10+
require 'optparse'
11+
12+
def parse_args
13+
options = {}
14+
15+
op = OptionParser.new do |opts|
16+
opts.banner = """Usage: #{opts.program_name} [option]..."""
17+
18+
opts.on("-o", "--output FILE", 'Output. Store results in file.') do |file|
19+
options[:output] = file
20+
end
21+
end
22+
23+
op.parse!
24+
25+
options
26+
end
27+
28+
29+
# Do the actual extraction of unique lines. By default, sort the hash
30+
# of values and line-number hash by line numbers; then return the
31+
# values. If given any other value for `order`, exploit Ruby's hash-
32+
# key order preservation to produce output containing the *first*
33+
# occurence of any given line.
34+
35+
def singularize(source, order=:last)
36+
form = {}
37+
38+
input = source.readlines
39+
input.each_with_index do |line, i|
40+
form[line.rstrip] = i
41+
end
42+
43+
if order == :last
44+
results = form.sort_by { |key, value| value }
45+
results.map(&:first)
46+
else
47+
form.keys
48+
end
49+
end
50+
51+
52+
# This takes the name of a destination file and a block.
53+
#
54+
# It creates a temporary file and proceeds to invoke the block,
55+
# passing the latter a handle to the temporary file. When the block
56+
# completes execution, it closes the file and renames so as to
57+
# overwrite the target.
58+
#
59+
# The original motivation was to make overwriting a file atomic. A
60+
# reader may see the old file, or the new file, but will never catch a
61+
# write in progress.
62+
63+
def atomic_write(target_name)
64+
temp_name = %x[mktemp].chomp
65+
handle = File.open(temp_name, 'w')
66+
yield(handle)
67+
handle.close
68+
File.rename(temp_name, target_name)
69+
end
70+
71+
# -----------------
72+
73+
options = parse_args
74+
75+
cmd_name = File.basename(__FILE__)
76+
77+
limit = (cmd_name == 'loo' ? :last : :first)
78+
79+
results = singularize(ARGF, limit)
80+
81+
if options[:output]
82+
atomic_write(options[:output]) { |handle| handle.puts results }
83+
else
84+
puts results
85+
end

path

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env ruby -w
2+
3+
# Present and manipulate paths.
4+
5+
require 'optparse'
6+
7+
class Path
8+
def initialize(*args)
9+
path_string = args.empty? ? ENV['PATH'] : args.join(":")
10+
@elements = path_string.split(/:/)
11+
end
12+
13+
def clean
14+
@elements.delete('')
15+
@elements.uniq!
16+
end
17+
18+
def show(separator)
19+
puts @elements.join(separator)
20+
end
21+
end
22+
23+
24+
def parse_args
25+
options = {}
26+
27+
op = OptionParser.new do |opts|
28+
opts.banner = <<-EOS.gsub(/^ */, '')
29+
Usage: #{opts.program_name} [option]... [argument]...
30+
31+
If called with non-flag command-line arguments, assemble them into a
32+
new PATH value and emit it. The current $PATH is not included unless
33+
explicitly mentioned: this lets you control the prepending or
34+
appending of new values.
35+
36+
In the absence of non-flag command-line arguments, emit the current
37+
$PATH.
38+
EOS
39+
40+
options[:clean] = false
41+
opts.on("-c", "--clean", "Clean path, removing any dupes.") do
42+
options[:clean] = true
43+
end
44+
45+
opts.on("-p", "--pack", "Pack path into one long line.") do
46+
options[:pack] = true
47+
end
48+
end
49+
50+
args = op.parse!
51+
52+
[args, options]
53+
end
54+
55+
args, options = parse_args
56+
57+
separator = options[:pack] ? ":" : "\n"
58+
59+
path = Path.new(*args)
60+
path.clean if options[:clean]
61+
path.show(separator)

0 commit comments

Comments
 (0)