Note that there are some explanatory texts on larger screens.

plurals
  1. POSort by function using bash/coreutils instead of perl
    primarykey
    data
    text
    <p>I found out that if you sort a list of files by file extension rather than alphabetically before putting them in a tar archive, you can dramatically increase the compression ratio (especially for large source trees where you likely have lots of .c, .o, and .h files).</p> <p>I couldn't find an easy way to sort files using the shell that works in every case the way I'd expect. An easy solution such as <code>find | rev | sort | rev</code> does the job but the files appear in an odd order, and it doesn't arrange them as nicely for the best compression ratio. Other tools such as <code>ls -X</code> don't work with <code>find</code>, and <code>sort -t. -k 2,2 -k 1,1</code> messes up when files have more than one period in the filename (e.g. version-1.5.tar). Another quick-n-dirty option, using <code>sed</code> replaces the last period with a <code>/</code> (which never occurs in a filename), then sorts, splitting along the <code>/</code>:</p> <pre><code>sed 's/\(\.[^.]*\)$/\/\1/' | sort -t/ -k 2,2 -k 1,1 | sed 's/\/\([^/]*\)$/\1/' </code></pre> <p>However, once again this doesn't work using the output from <code>find</code> which has <code>/</code>s in the names, and all other characters (other than 0) are allowed in filenames in *nix.</p> <p>I discovered that using Perl, you can write a custom comparison subroutine using the same output as <code>cmp</code> (similar to <code>strcmp</code> in C), and then run the perl sort function, passing your own custom comparison, which was easy to write with perl regular expressions. This is exactly what I did: I now have a perl script which calls</p> <pre><code>@lines = &lt;STDIN&gt;; print sort myComparisonFunction @lines; </code></pre> <p>However, perl is not as portable as bash, so I want to be able to do with with a shell script. In addition, <code>find</code> does not put a trailing / on directory names so the script thinks directories are the same as files without an extension. Ideally, I'd like to have <code>tar</code> read all the directories first, then regular files (and sort them), then symbolic links which I can achieve via</p> <pre><code>cat &lt;(find -type d) &lt;(find -type f | perl exsort.pl) &lt;(find -not -type d -and -not -type f) | tar --no-recursion -T - -cvf myfile.tar </code></pre> <p>but I still run into the issue that either I have to type this monstrosity every time, or I have both a shell script for this long line AND a perl script for sorting, and perl isn't available everywhere so stuffing everything into one perl script isn't a great solution either. (I'm mainly focused on older computers, cause nowadays all modern Linux and OSX come with a recent enough version of perl).</p> <p>I'd like to be able to put everything together into one shell script, but I don't know how to pass a custom function to GNU sort tool. <b>Am I out of luck, and have to use one perl script? Or can I do this with one shell script?</b></p> <p><b>EDIT:</b> Thanks for the idea of a Schwartizan Transform. I used a slightly different method, using <code>sed</code>. My final sorting routine is as follows:</p> <pre><code>sed 's_^\(\([^/]*/\)*\)\(.*\)\(\.[^\./]*\)$_\4/\3/\1_' | sed 's_^\(\([^/]*/\)*\)\([^\./]\+\)$_/\3/\1_' | sort -t/ -k1,1 -k2,2 -k3,3 | sed 's_^\([^/]*\)/\([^/]*\)/\(.*\)$_\3\2\1_' </code></pre> <p>This handles special characters (such as *) in filenames and places files without an extension first because they are often text files. (Makefile, COPYING, README, configure, etc.).</p> <p>P.S. In case anyone wants my original comparison function or think I could improve on it, here it is:</p> <pre><code>sub comparison { my $first = $a; my $second = $b; my $fdir = $first =~ s/^(([^\/]*\/)*)([^\/]*)$/$1/r; my $sdir = $second =~ s/^(([^\/]*\/)*)([^\/]*)$/$1/r; my $fname = $first =~ s/^([^\/]*\/)*([^\/]*)$/$2/r; my $sname = $second =~ s/^([^\/]*\/)*([^\/]*)$/$2/r; my $fbase = $fname =~ s/^(([^\.]*\.)*)([^\.]*)$/$1/r; my $sbase = $sname =~ s/^(([^\.]*\.)*)([^\.]*)$/$1/r; my $fext = $fname =~ s/^([^\.]*\.)*([^\.]*)$/$2/r; my $sext = $sname =~ s/^([^\.]*\.)*([^\.]*)$/$2/r; if ($fbase eq "" &amp;&amp; $sbase ne ""){ return -1; } if ($sbase eq "" &amp;&amp; $fbase ne ""){ return 1; } (($fext cmp $sext) or ($fbase cmp $sbase)) or ($fdir cmp $sdir) } </code></pre>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload