How can I make “ls” show dotfiles first while staying case-insensitive?
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
.
You can use the shell's sort order instead (which may not involve the locale's collation order; bash
, AT&T ksh
, yash
, tcsh
and zsh
give the expected results, mksh
and dash
don't. fish
seems to give a case insensitive order but gives different results when there are non-ASCII characters):
ls -dUl -- .* *
This gives ls
an explicit list of files (and directories) to list, and deactivates ls
's sorting (-U
, which is a GNU extension).
There are a few caveats, depending on the shell you're using.
- With
zsh
, the defaultnomatch
option will cause the command to fail if the directory doesn't contain both hidden and non-hidden files; you could disablenomatch
to avoid that, but better would be to doset -o cshnullglob
instead (and the command to fail only if none of the globs match like in(t)csh
or early Unix shells). - With
zsh
,pdksh
and its derivative andfish
,.*
's expansion doesn't include.
and..
, so this matchesls -Al
. With other shells.
and..
are included so it matchesls -al
. In the latter case you'd need to change the globbing patterns to exclude.
and..
(ls -dUl -- ..?* .[!.]* *
). - Except in
fish
,(t)csh
orzsh
, if any of the globbing patterns don't match anything,ls
will produce an error message; you can avoid this either by setting thenullglob
option (inbash
orzsh
at least), or by redirectingstderr
to/dev/null
(ls -dUl -- ..?* .[!.]* * 2>/dev/null
). If you usenullglob
, watch out for the potentially-surprising behaviour that causes (see Shell eating `?` characters).fish
behaves likebash
withnomatch
except that when interactive, a warning message will be issued for each glob that has no match.
(With thanks to Stéphane Chazelas for all the feedback!)
You might simply use two separate ls
commands:
$ ls -dl ..?* .[^.]* 2>/dev/null ; ls -dl *
-rw-r--r--. 1 sparhawk sparhawk 0 8 Jun 09:29 .a
-rw-r--r--. 1 sparhawk sparhawk 0 8 Jun 09:29 .b
-rw-r--r--. 1 sparhawk sparhawk 0 8 Jun 09:29 a
-rw-r--r--. 1 sparhawk sparhawk 0 8 Jun 09:29 A
-rw-r--r--. 1 sparhawk sparhawk 0 8 Jun 09:29 b
-rw-r--r--. 1 sparhawk sparhawk 0 8 Jun 09:29 B
-rw-r--r--. 1 sparhawk sparhawk 0 8 Jun 09:29 你好嗎
Unlike the other answers so far, this approach displays the dot files first avoiding the .
and ..
entries, then the remaining entries in ls
alphabetical order.
@StephenKitt answer's might be improved though to achieve the same result:
$ ls -dUl ..?* .[^.]* * 2>/dev/null