Shell Magic: Set Operations with uniq
blog.deadvax.net
blog.deadvax.net
https://en.wikipedia.org/wiki/Comm
# show only items in both a and b
comm -1 -2 a_list b_list
# show only items unique to a
comm -2 -3 a_list b_list
# show only items unique to b
comm -1 -3 a_list b_list comm <flags> <(command 1) <(command 2)
To use the output of a command as input.
This also works with diff and other commands.The '<( ... )' is just giving the path to that command's stdout as a file descriptor.
$ ls <(echo hi)
/proc/self/fd/11
$ vi <(echo hi)
# opens vi with 'hi' as the contentshttp://www.tldp.org/LDP/abs/html/process-sub.html
With fish:
https://fishshell.com/docs/current/commands.html#psub
But there it only works one way:
[0] https://gist.github.com/hadrianw/060944011acfcadd889d937b960...
# show only items in both a and b
comm -1 -2 <(sort -u a_list) <(sort -u b_list)
# show only items unique to a
comm -2 -3 <(sort -u a_list) <(sort -u b_list)
# show only items unique to b
comm -1 -3 <(sort -u a_list) <(sort -u b_list)What the blog post does (doing cat multiple times, and then counting the occurrences) is going to be much slower, although mathematically correct.
I think the same thing happens to a lesser degree with vim features.
Other boring, but instructive reads (heavily GNU biased): diffutils [1], findutils [2], the Bash manual [3].
[0]: https://www.gnu.org/software/coreutils/manual
[1]: https://www.gnu.org/software/diffutils/manual
[2]: https://www.gnu.org/software/findutils/manual/find.html
comm "not only in 1" and "not in both"
so:
comm -1 -3 file1 file2
Union: Instead of
cat a_list b_list | sort | uniq
do sort -m a_list b_list | uniq
Intersection: Instead of cat a_list b_list | sort | uniq -c | grep 2\
do sort -m a_list b_list | uniq -d
Relative complement: Instead of cat a_list b_list b_list | sort | uniq -c | grep 2\
do sort -m a_list a_list b_list | uniq -u
Note the change of approach here: instead of making lines from b_list appear twice and grepping for that count, make lines from a_list appear twice and have uniq only print lines that aren't repeated.I prefer `awk` over `uniq` and `comm` because awk tends to be faster at set ops that can skip sorting and deduplicating.
Here's my script for union, intersection, etc. See README on GitHub. Suggestions welcome.
https://github.com/sixarm/setop
#!/bin/sh
set -eu
op="$1"; shift
case $op in
∪|u|union|or|∨|add|addition|'+'|'|')
awk '!seen[$0] {print} {seen[$0]=1}' "$@"
;;
∩|i|intersection|and|∧|'&')
awk 'FNR==1{argind+=1} seen[$0]+=1 {next} END { for (key in seen) { if (seen[key]==argind) { print key } } }' "$@"
;;
⊖|d|diff|difference|xor|⊻)
awk 'FNR==1{argind+=1} seen[$0]+=1 {next} END { for (key in seen) { if (seen[key]==1) { print key } } }' "$@"
;;
ex|except|exclude|subtract|subtraction|'-')
awk 'NR==FNR{seen[$0]=1;next} seen[$0]=0; END { for (key in seen) { if (seen[key]) { print key } } }' "$@"
;;
extra)
awk 'BEGIN{argindmax=ARGC-1} FNR==1{argind+=1} argind<argindmax {seen[$0]; next}!($0 in seen)' "$@"
;;
disjoint|n|not|none)
awk 'seen[$0]==1 {x=1;exit} {seen[$0]=1} END { print x ? "FALSE" : "TRUE"}; exit !!x}' "$@"
;;
*)
esacWe benefit from simple analytics that are POSIX, not python, perl, R, etc. http://www.numcommand.com/
Yes in my experience of awk/gawk on Windows, things work the same, other than file separators, line endings, and similar platform-specific issues.
For your second question, awk simply works. It's reliable, fast, and everywhere. I also like python and perl, and choose these for any modern system.
For comparison, a typical POSIX `uniq` implementation reads the input and solely compares two adjacent lines; this requires the input to be presorted.
An interesting upgrade could be to add a `setop` option flag that tells the script the inputs are already sorted and/or deduped. This can achieve the memory savings you're describing.
I mean without resorting to awk.
Those mathematical notations, are you using them because it makes it easier to see how it corresponds to actual Set Theory/theorems? If so, could you just as well have used an alphanummeric identifier like "left" "union" "right" or - would the code break without this notation? I'm on deep waters here, I don't know this. But set theory seems to pop up a lot in my line of work, essentially doing joins in datasets using Tableau - so my interest in the nitty gritty of this field is increasing.
> because it makes it easier to see how it corresponds to actual Set Theory/theorems?
Yes. These are the Unicode symbols for set theory.
> could you just as well have used an alphanummeric identifier like "left" "union" "right" or
Yes. You can use any of the words in the case switch statements, such as `setop union file1 file2`. You can also edit the script to add your own words if you like.
You can see simpler versions of these scripts in our GitHub repos. For example the `union` command is https://github.com/sixarm/union
> set theory seems to pop up a lot in my line of work
More and more in mine too. Thank you for your comments!
http://www.pixelbeat.org/cmdline.html#sets
Note comm output is a bit awkward to parse, so I use another coreutils `join` command to process already sorted data
Another way to get the last date in current month (== the number of days in a month) is:
: $(cal); echo $_ if whatevs; then
:
fi‘:’ is the command, and ‘$(cal)’ – which equals the unquoted output from running the ‘cal’ command – are the arguments. The last day of the month is thus the last argument of ‘:’ and can be referenced with the ‘$_’ variable.
It's too disgusting to share.
An inefficient solution which involves unnecessary sorting: for each file_i, 0 <= i < n in the set of n files, cat it 2^i times before combining to pipe through sort and uniq -c. Every possible set operation combination can be determined by grepping the result for a particular combination of counts. Intersection would calculated by grepping for 2^n - 1 while symmetric difference would require egrep to pick out any of 1, 2, 4,..,2^(n-1).
1. Use sed to add "1<tab>" (that's a one digit and a tab char) to the first list to difference by and save as "prefix.txt".
2. Use cat to combine all the lists, sort | uniq -c | sort -n | sed <reformat to make tab delimited> | sort again and save as output.txt.
3. join prefix.txt and output.txt on each whole line and cut the second tab delimited field to produce the final result.
So in order to be in the result, a list item must appear in exactly one list and that must be the first list. That should be what we want (?)
https://sources.debian.org/src/corekeeper/1.6/debian/corekee...
It's actually possible to simplify the set operations a little in that case.
rel_com() {
for i in $(seq 1 $#); do
for j in $(seq 1 $(bc <<< "2 ^ $(expr $i - 1)")); do
echo ${!i}
done
done
}
sort -m $(rel_com a b c) | uniq -c
Then you have a N-bit number with the i'th bit representing membership in the i'th file. diff -u <(cat file1) <(cat file2)
Obviously, the 'cat' commands can be more complex commands, which make this more interesting. # sort a_list b_list | uniq
a
b
c
d
eSort accepts a list of files as arguments. "sort [flags] file1 file2 ..." is more concise, more efficient, and is less likely to run out of space.
</dir/file sort | uniq >outfile
foo bar < a
foo < a bar
< a foo bar > a # from stdout to file
< a # simply the opposite operation: from file to stdin