LS Command – How to Show Dotfiles First While Staying Case-Insensitive

localels

Create the following files in a directory.

$ touch .a .b a b A B 你好嗎

My default ls order ignores the presence of leading dots, intermingling them with the other files.

$ ls -Al
total 0
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:03 a
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:03 .a
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:03 A
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:03 b
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:03 .b
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:03 B
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:06 你好嗎

I can change LC_COLLATE to put the dotfiles first.

$ LC_COLLATE=C ls -Al
total 0
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:03 .a
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:03 .b
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:03 A
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:03 B
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:03 a
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:03 b
-rw-r--r-- 1 sparhawk sparhawk 0 Jun  8 17:06 你好嗎

Unfortunately this makes the sort order case-sensitive, i.e. A and B precede a and b. Is there a way to print dotfiles first while staying case-insensitive (A and a precede B and b)?

Edit: attempting to modify LC_COLLATE

None of the answers so far fully replicate the functionality of ls easily. Conceivably, I could wrap some of them in a function, but this would have to include some detailed code on (e.g.) how to work with no argument vs. supplying a directory as an argument. Or how to deal with an explicit -d flag.

Alternatively, I thought that maybe there could be a better LC_COLLATE to use. However, I can't seem to make that work. I'm currently using LC_COLLATE="en_AU.UTF-8". I checked /usr/share/i18n/locales/en_AU (although I'm not sure if this is the right file, as I can't see any reference to UTF-8); I found the following.

LC_COLLATE
copy "iso14651_t1"
END LC_COLLATE

/usr/share/i18n/locales/iso14651_t1 contains copy "iso14651_t1_common". Finally, /usr/share/i18n/locales/iso14651_t1_common contains

 <U002E> IGNORE;IGNORE;IGNORE;<U002E> # 47 .

I deleted this line, ran sudo locale-gen, and restarted my computer. Unfortunately, this changed nothing.

Best Answer

OP was very close with editing /usr/share/i18n/locales/iso14651_t1_common, but the trick is not to delete the line

<U002E> IGNORE;IGNORE;IGNORE;<U002E> # 47 .

but rather to modify it to

<U002E> <RES-1>;IGNORE;IGNORE;<U002E> # 47 .

Why this works

The IGNORE statements specify that the full stop (aka period, or character <U002E>) will be ignored when ordering words alphabetically. To make your dotfiles come first, change IGNORE to a collating symbol that comes before all other characters. Collating symbols are defined by lines like

collating-symbol <something-inside-angle-brackets>

and they are ordered by the appearance of the line

<something-inside-angle-brackets>

In my copy of iso14651_t1_common, the first-place collating symbol is <RES-1>, which appears on line 3458. If you file is different, use whichever collating symbol is ordered first.

Details about character ordering with LC_COLLATE

<U002E> has three IGNORE statements because letters can be compared multiple times in case of ties. To understand this, consider lowercase a and uppercase A (which are part of a group of characters that actually get compared four times):

<U0061> <a>;<BAS>;<MIN>;IGNORE # 198 a
<U0041> <a>;<BAS>;<CAP>;IGNORE # 517 A

Having multiple rounds of comparison allow files that start with "a" and "A" to be grouped together because both are compared as <a> during the first pass, with the next letter determining the ordering. If all of the following letters are the same (e.g. a.txt and A.txt), the third pass will put a.txt first because the collating symbol for lowercase letters <MIN> appears on line 3467, before the collating symbol for uppercase letters <CAP> (line 3488).

Implementing this change

If you want the period to come first every time a program orders letters using LC_COLLATE, you can modify iso14651_t1_common as described above and rebuild your locations file. But if you want to make this change only to ls and without root access, you can copy the original locale files to another directory before modifying them.

What I did

My default locale is en_US, so I copied en_US, iso14651_t1, and iso14651_t1_common to $HOME/path/to/new/locales. There I made the abovementioned change to iso14651_t1_common and renamed en_US to en_DOTFILE. Next I compiled the en_DOTFILE locale with

localedef -i en_DOTFILE -f UTF-8 -vc $HOME/path/to/new/locales/en_DOTFILE.UTF-8

To replace the default ls ordering, make a BASH script called ls:

#!/bin/bash
LOCPATH=$HOME/path/to/new/locales LANG=en_DOTFILE.UTF-8 ls "$@"

save it somewhere that appears before /usr/bin on your path, and make it executable with chmod +x ls.