Guides

ls command in Linux: read ls -l, show hidden files, sort by size and list only directories

ls command in Linux: read ls -l columns, show hidden files, sort by size or date, list only directories, and why scripts should not parse ls.

Published ~12 min read
On this page

Short answer

  • ls lists the files in the current directory, ls /var/log the ones in another directory.
  • ls -la adds hidden files (names that start with a dot) and shows permissions, owner, size and date for each entry.
  • ls -lh prints sizes as 4.0K, 25M or 1.1G instead of bytes; add S (ls -lhS) to put the biggest file first.

Key facts

Package
GNU coreutils
Binary
/usr/bin/ls
Version in this guide
9.1 (Debian 12)Debian 13: 9.7, Ubuntu 24.04: 9.4
Ubuntu 25.10 and later
rust-coreutils (uutils)Ubuntu Server docs and 26.10 release notes
Alpine and BusyBox-based images
BusyBox lsfewer options, see below
Exit status
0 OK, 1 minor problem, 2 serious troubleman ls
any Linux
ls                 # names in the current directory
ls -la             # long list, hidden files included
ls -lh             # human-readable sizes
ls -lhS            # biggest first
ls -ltr            # oldest first, newest at the bottom
ls -d */           # only directories
ls -R              # everything below, directory by directory

Every output block below was run on one of our Debian 12 servers with GNU coreutils 9.1 and bash 5.2.15, inside a small test tree in a scratch directory (a web root with a log, a backup, a symlink called current and a file with a space in its name). The other systems follow their manuals and package pages, named in the text.

How to read ls -l output, column by column

Output · ls -la
$ ls -la
total 40
drwxr-xr-x 6 root root      4096 Nov  2 02:31 .
drwxr-xr-x 4 root root      4096 Nov  2 03:13 ..
drwxr-xr-x 2 root root      4096 Nov  1 12:00 .git
-rw-r--r-- 1 root root         2 Nov  2 02:30 .htaccess
-rw-r--r-- 1 root root 146800640 Mar 14  2025 backup.tar.gz
lrwxrwxrwx 1 root root        19 Nov  1 12:00 current -> releases/2026-11-01
-rw-r--r-- 1 root root      1080 Oct 30 09:15 index.html
drwxr-xr-x 3 root root      4096 Nov  1 22:40 logs
-rw-r--r-- 1 root root         6 Nov  2 02:30 my notes.txt
drwxr-xr-x 3 root root      4096 Nov  1 12:00 releases
-rwxr-xr-x 1 root root        18 Nov  2 02:30 run.sh
drwxr-xr-x 2 root root      4096 Oct 12 17:05 uploads

Take one line apart, the one for the shell script:

one line of ls -l, annotated
-rwxr-xr-x  1  root  root  18  Nov  2 02:30  run.sh
|\_/\_/\_/  |   |     |    |   |             |
| |  |  |   |   |     |    |   |             name (symlinks add "-> target")
| |  |  |   |   |     |    |   modification time
| |  |  |   |   |     |    size in bytes
| |  |  |   |   |     group
| |  |  |   |   owner
| |  |  |   hard link count
| |  |  others: r-x
| |  group: r-x
| owner: rwx
file type: - regular file
ColumnExampleWhat it tells you
File type- d l c b p sRegular file, directory, symbolic link, character device, block device, named pipe, socket.
Permissionsrwxr-xr-xRead, write, execute for owner, group and everyone else. An s in place of an x is setuid or setgid (/usr/bin/passwd shows -rwsr-xr-x), a t at the end is the sticky bit (/tmp shows drwxrwxrwt). A capital S or T means the special bit is set without the x under it (-rwSr--r-- after chmod 4644).
Extra characterrw-r--r--+A + after the permissions means an ACL grants more access than the bits show; GNU ls prints a . for a file that only carries an SELinux context.
Links3Hard links to the file. For a directory on ext4 it is 2 plus the number of subdirectories, which is why logs (one subdirectory) shows 3.
Owner, grouproot rootNames from /etc/passwd and /etc/group. ls -n prints the numeric IDs instead (0 0), which is what you want when a backup restored files for a user that does not exist on this machine.
Size18Bytes. A symlink shows the length of its target path (current is 19, the length of releases/2026-11-01), a device file shows major and minor number (1, 3 for /dev/null), a directory shows the size of its own entry list (usually 4096), never the size of its contents.
TimeNov 2 02:30Last modification. Files older than six months, or dated in the future, show the year instead of the clock time (Mar 14 2025), per POSIX.

The total line is not the sum of the sizes. It is the disk space the listed files occupy, in blocks of 1 KiB with GNU ls. In the test tree, backup.tar.gz reports 140 MiB yet the total is 40, because that file (and logs/app.log) was created with truncate and is sparse: it has a size but no blocks on disk yet. VM disk images and some database files behave the same way. ls -s shows the allocated blocks per file, and du agrees with it:

Output
$ ls -l logs/app.log; du -h logs/app.log; du -h --apparent-size logs/app.log
-rw-r--r-- 1 root root 26214400 Nov  1 22:40 logs/app.log
0	logs/app.log
25M	logs/app.log

Two variants of the long format help in scripts and audits: ls -g leaves out the owner, ls -o the group. To change what the owner and group columns say, see the chown guide.

ls hidden files: -a and -A

Linux has no hidden attribute. A name that starts with a dot is left out of a plain ls, and that is all “hidden” means. Three ways to see them:

Output
$ ls -a
.
..
.git
.htaccess
backup.tar.gz
...
$ ls -A
.git
.htaccess
backup.tar.gz
...
$ ls -ld .[!.]*
drwxr-xr-x 2 root root 4096 Nov  1 12:00 .git
-rw-r--r-- 1 root root    2 Nov  2 02:30 .htaccess
  • ls -a (all) shows everything, including . (this directory) and .. (the parent).
  • ls -A (almost all) leaves out . and .., which is the one you want most of the time.
  • ls -ld .[!.]* shows only the hidden entries. The -d matters: without it, ls would print the contents of .git instead of the line for .git itself. Since bash 5.2 the simpler ls -d .* also works, because the new default globskipdots no longer lets .* match . and ..; bash before 5.2 lists both.

The same rule applies to the shell: * skips dot files, so rm * or cp * leaves .htaccess and .env behind. Moving a website to a new server is where that bites; shopt -s dotglob makes bash include them.

Sort by size, time, extension or version

With no option, ls sorts by name. The order follows your locale: our server runs LANG=C.UTF-8, which sorts by character code, so capitals come before lower case. Locales such as en_US.UTF-8 use dictionary order, and LC_COLLATE=C ls forces the byte order anywhere.

ls sort by size: -S

Output
$ ls -lhS
total 24K
-rw-r--r-- 1 root root 140M Mar 14  2025 backup.tar.gz
drwxr-xr-x 3 root root 4.0K Nov  1 22:40 logs
drwxr-xr-x 3 root root 4.0K Nov  1 12:00 releases
drwxr-xr-x 2 root root 4.0K Oct 12 17:05 uploads
-rw-r--r-- 1 root root 1.1K Oct 30 09:15 index.html
lrwxrwxrwx 1 root root   19 Nov  1 12:00 current -> releases/2026-11-01
-rwxr-xr-x 1 root root   18 Nov  2 02:30 run.sh
-rw-r--r-- 1 root root    6 Nov  2 02:30 my notes.txt

Biggest first; add -r for smallest first. Note the three directories at 4.0K: ls sorts them by the size of the directory entry, not by what is inside. To find which directory is big, ask du:

Output
$ du -sh */
8.0K	current/
8.0K	logs/
4.0K	releases/
180K	uploads/

On a full disk, the guide to checking disk usage on Linux takes df and du from the whole filesystem down to the one directory to clean up.

Sort by date: -t, -ltr and the newest file

Output
$ ls -ltr
total 24
-rw-r--r-- 1 root root 146800640 Mar 14  2025 backup.tar.gz
drwxr-xr-x 2 root root      4096 Oct 12 17:05 uploads
-rw-r--r-- 1 root root      1080 Oct 30 09:15 index.html
drwxr-xr-x 3 root root      4096 Nov  1 12:00 releases
lrwxrwxrwx 1 root root        19 Nov  1 12:00 current -> releases/2026-11-01
drwxr-xr-x 3 root root      4096 Nov  1 22:40 logs
-rwxr-xr-x 1 root root        18 Nov  2 02:30 run.sh
-rw-r--r-- 1 root root         6 Nov  2 02:30 my notes.txt

-t sorts newest first; -ltr reverses it so the newest file sits at the bottom, right above your prompt, which is the habit for log and backup directories. -t uses the modification time. Combine it with -u to sort by last access, or with -c for the last status change (permissions, owner, rename). Neither is a creation time.

For the creation (birth) time, GNU ls has --time=birth since coreutils 8.32: ls -l --time=birth shows it and ls -lt --time=birth sorts by it, where the filesystem records one (ext4 does). In the test tree the files were created today and then given older dates with touch -d, so the two columns differ:

Output · ext4
$ ls -l backup.tar.gz index.html
-rw-r--r-- 1 root root 146800640 Mar 14  2025 backup.tar.gz
-rw-r--r-- 1 root root      1080 Oct 30 09:15 index.html
$ ls -l --time=birth backup.tar.gz index.html
-rw-r--r-- 1 root root 146800640 Nov  2 03:41 backup.tar.gz
-rw-r--r-- 1 root root      1080 Nov  2 03:41 index.html

stat shows the same value on its Birth: line. Note that a copy or a restore from backup is a new file with a new birth time, even when it keeps the old modification time.

By extension, version or not at all

Output
$ ls
backup-1.sql
backup-10.sql
backup-11.sql
backup-2.sql
backup-9.sql
$ ls -v
backup-1.sql
backup-2.sql
backup-9.sql
backup-10.sql
backup-11.sql
  • -v sorts numbers inside names naturally, so backup-10 comes after backup-9.
  • -X groups by extension (everything without one first, then .gz, .html, .sh, .txt).
  • --group-directories-first keeps directories (and symlinks to them) at the top and still sorts each group by the other options.
  • -U does not sort at all and prints entries in directory order, which is the fast choice in a directory with hundreds of thousands of files.

Human-readable sizes: -h, --si and --block-size

Output
$ ls -lh backup.tar.gz
-rw-r--r-- 1 root root 140M Mar 14  2025 backup.tar.gz
$ ls -lh --si backup.tar.gz
-rw-r--r-- 1 root root 147M Mar 14  2025 backup.tar.gz
$ ls -l --block-size=K index.html "my notes.txt"
-rw-r--r-- 1 root root 2K Oct 30 09:15 index.html
-rw-r--r-- 1 root root 1K Nov  2 02:30 my notes.txt

-h counts in powers of 1024 (the M means MiB), --si in powers of 1000, the way disk vendors count. --block-size=K, M or G fixes the unit for every line so the column lines up, but it rounds up: a 6-byte file shows as 1K and a 1,080-byte file as 2K.

ls only directories

Output
$ ls -d */
current/
logs/
releases/
uploads/
$ ls -p | grep /
logs/
releases/
uploads/
$ find . -mindepth 1 -maxdepth 1 -type d -printf '%f\n'
releases
.git
uploads
logs

Three answers, and they disagree on purpose:

  • ls -d */: the shell expands */ to every directory, and -d tells ls to print those names instead of opening them. It includes current, a symlink that points to a directory, and skips hidden directories. Add .*/ (ls -d */ .*/) to include them, or use ls -ld */ for the long format.
  • ls -p | grep /: -p appends a slash to directories only, so the symlink drops out. ls -p | grep -v / is the opposite: everything that is not a directory.
  • find -type d: real directories only, hidden ones included, no symlinks, in directory order rather than sorted. Pipe through sort if order matters; the find guide covers the rest of its tests.

Avoid ls -l | grep '^d' in scripts: it works for a look, but it parses ls output, which the section on parsing ls explains.

ls recursive: ls -R vs tree vs find

Output
$ ls -R
.:
backup.tar.gz
current
index.html
logs
my notes.txt
releases
run.sh
uploads

./logs:
app.log
old

./logs/old:

./releases:
2026-11-01

./releases/2026-11-01:

./uploads:
photo.jpg

ls -R lists one directory, then each subdirectory under its own heading, all the way down. ls -lR does the same in long format with a total per directory. It does not follow the current symlink, so the release is listed once. Good for a quick look at a small tree; on a big one it floods the screen: ls -R /usr printed 90,139 lines on our test server.

ls -Rtreefind
Installed on Debian and Ubuntu by defaultYesNoYes
Full path on every lineNo-fYes
Limit the depthNo-L 2-maxdepth 2
Filter by name, size or ageNoname only (-P)Yes
NUL-separated names for scriptsNoNo-print0
Best fora quick lookshowing structureanything you act on
tree is the separate tree package (apt install tree). Checked against man ls, man find and the tree manual on November 2, 2026.

To search the tree rather than read it, for files over 100 MiB or files changed today, use find; it was built for that job.

List files by pattern: ls *.log, *.conf, *.d

Output
$ ls *.txt
my notes.txt
$ ls -l /etc/*.conf | head -3
-rw-r--r-- 1 root root  3040 May 25  2023 /etc/adduser.conf
-rw-r--r-- 1 root root  6979 Oct 22 06:56 /etc/ca-certificates.conf
-rw-r--r-- 1 root root  2969 Jan  8  2023 /etc/debconf.conf
$ ls -d /etc/*.d | head -3
/etc/apparmor.d
/etc/bash_completion.d
/etc/binfmt.d
$ ls *.pdf
ls: cannot access '*.pdf': No such file or directory

ls itself knows nothing about *. The shell expands the pattern to the matching names before ls starts, and ls lists exactly those. Three things follow from that:

  • A pattern that matches a directory lists its contents. ls /etc/*.d prints what is inside every .d directory; -d (ls -d /etc/*.d) prints the directory names.
  • When nothing matches, bash passes the pattern on unchanged, and ls reports cannot access '*.pdf' with exit status 2.
  • Patterns do not search subdirectories: ls *.log only looks in the current directory. For a whole tree, use find . -name '*.log'; with many thousands of matches, see Argument list too long below.

Useful ls options by task

TaskCommandNote
One name per linels -1Already the default when output goes into a pipe or a file
Columns, sorted downls -CThe default in a terminal; forces columns into a pipe too
Columns, sorted acrossls -xSame as --format=across; other values: commas, long, single-column, vertical
Set the line widthls -x -w 40Overrides the terminal width and COLUMNS; -w 0 means no limit
Files matching a patternls -l *.confThe shell expands the pattern, see by pattern
Sort by creation timels -lt --time=birthGNU coreutils 8.32 and later, on filesystems that record it
Mark type after the namels -F/ directory, @ symlink, * executable
Show inode numbersls -liSame number on two names = hard links to one file
The directory itself, not its contentsls -ld /var/wwwCheck permissions of a folder
Follow a symlink to a directoryls -l current/The trailing slash (or -L) lists the target
Full timestampls -l --full-timeSeconds, nanoseconds and time zone
ISO datesls -l --time-style=long-isoPrints 2025-03-14 08:00 for every line
Custom date formatls -l --time-style=+'%F %T'Any date(1) format after the +
Numeric owner and groupls -nUID and GID instead of names
Hide a patternls -I '*.log'--hide does the same but gives way to -a
Hide editor backupsls -BLeaves out names that end in ~, such as run.sh~
SELinux contextls -lZAdds the security context; prints ? where none is set, as on a stock Debian server
Comma-separatedls -mFills the line, handy for pasting
Names in double quotesls -QOr ls -b to escape spaces and newlines
Unsorted, fastestls -UDirectory order
NUL-separated namesls --zeroGNU coreutils 9.0 and later only
Help and versionls --helpman ls for the manual, ls --version for GNU or uutils and the version

ls aliases and colors on Debian and Ubuntu

If ls shows colors for you but not for root, or ll works on one server and not the next, the difference is the shell startup file, not ls. New users on both systems get /etc/skel/.bashrc, which sets:

/etc/skel/.bashrc
alias ls='ls --color=auto'      # Debian 12 and Ubuntu 24.04
alias ll='ls -alF'              # Ubuntu (Debian 12: #alias ll='ls -l')
alias la='ls -A'                # Ubuntu: on; Debian: commented out
alias l='ls -CF'                # Ubuntu: on; Debian: commented out

Root on Debian gets its .bashrc from base-files, where every ls line (LS_OPTIONS, dircolors, alias ls, ll, l) is commented out, so a stock Debian install shows plain, uncolored output to root until you remove the #. Check what you are running and bypass an alias for one call:

bash
type ls          # ls is aliased to `ls --color=auto'  (or: ls is /usr/bin/ls)
\ls -l           # backslash: skip the alias once
command ls -l    # same, and works for functions too
  • --color=auto only colors a terminal. Pipe ls into less or grep and the colors disappear; ls --color=always | less -R keeps them. The default palette from dircolors is bold blue for directories, bold cyan for symlinks and bold green for executables; ~/.dircolors overrides it.
  • No colors even with --color=always? Since coreutils 8.23, with neither LS_COLORS nor COLORTERM set, ls honors an empty or unknown TERM. With TERM=dumb our test printed plain names. Some minimal containers and serial consoles start that way.
  • Why 'my notes.txt' has quotes. Since coreutils 8.25, ls quotes names with spaces or special characters when it writes to a terminal, so the name can be pasted back into a command. When one name in the listing is quoted, the others get a space in front to keep the columns aligned. ls -N or export QUOTING_STYLE=literal turns it off. By default, pipes and files get no quotes; -Q, -b, --quoting-style or an exported QUOTING_STYLE quote there too.
Output in a terminal, 80 columns
$ ls
 backup.tar.gz	 index.html  'my notes.txt'   run.sh
 current	 logs	      releases	      uploads
$ ls -N
backup.tar.gz  index.html  my notes.txt  run.sh
current        logs	   releases	 uploads

Why you should not parse ls output

ls output is made for people. A file name on Linux may contain spaces, tabs and even a newline, and ls prints them as they are when it writes into a pipe. Two lines that look fine and break:

Output
$ for f in $(ls); do echo "[$f]"; done
[backup.tar.gz]
...
[my]
[notes.txt]
...
$ touch $'bad\nname.txt'
$ ls | wc -l
10

The loop split my notes.txt into two words, and the count says 10 for 9 visible entries because the newline in one name made two lines. A common tip, piping find into xargs ls -l, fails the same way:

Output
$ find . -name '*.txt' | xargs ls -l
ls: cannot access './my': No such file or directory
ls: cannot access 'notes.txt': No such file or directory
$ find . -name '*.txt' -exec ls -l {} +
-rw-r--r-- 1 root root 6 Nov  2 02:30 ./my notes.txt

What to use instead, each tested on the same tree:

bash
for f in *; do echo "[$f]"; done                     # loop: let the shell expand the glob
shopt -s nullglob dotglob; set -- *; echo "$#"       # count entries, hidden ones included
find . -mindepth 1 -maxdepth 1 -printf '.' | wc -c   # count, safe with newlines in names
stat -c '%s %y %n' -- *                              # size and date as fields you choose
find . -maxdepth 1 -type f -printf '%s\t%p\n' | sort -nr   # files by size, for a script

stat and find -printf give you exactly the fields you ask for, in a format that does not change with the locale or the age of the file (remember that the ls date column switches from time to year after six months). For a single file, stat -c '%A %U %G %s %y %n' index.html prints the same facts as one ls -l line, without the guesswork. GNU ls 9.0 added --zero for NUL-separated output, but it is not in BusyBox or older systems, so find stays the portable choice.

GNU ls vs BusyBox ls (Alpine and Docker images)

Inside an Alpine container, other BusyBox-based Docker images and initramfs shells, ls is BusyBox, one small binary that implements the common options and drops the rest. A script that runs fine on the host can then stop with an unknown option:

GNU ls (Debian, Ubuntu 24.04)BusyBox ls
-a -A -l -h -S -t -r -R -d -i -F -p -X -vYesYes
--full-time, --group-directories-firstYesYes
--colorYesYes
--time-styleYesNo
-I / --hideYesNo
-Q, -m, -N, -bYesNo
--block-size, --siYesNo
-g, -o, -UYesNo
--zero9.0 and laterNo
BusyBox column: the ls help text of BusyBox 1.35 in Debian's busybox package. Alpine compiles BusyBox with its own configuration, so run ls --help in the container to see its list.

For scripts that have to run in both places, stick to the options in the first three rows, or call find and stat (both also exist in BusyBox, with fewer options). On Ubuntu 25.10 and later there is a third implementation: Canonical made rust-coreutils (uutils) the default, ls included, and kept a few tools such as cp, mv and rm on GNU in 26.04 LTS. Ubuntu 26.10 (October 15, 2026) moved those last ones to uutils as well; the coreutils-from-gnu package switches a system back to GNU. uutils aims at GNU compatibility; ls --version tells you which one you have before you rely on an edge case.

ls errors and what they mean

Symptom, cause, fix

  1. Symptom: ls: cannot access 'missing.txt': No such file or directory

    Cause
    A typo, a relative path from the wrong directory, or a glob such as *.log that matched nothing, so the shell passed it on literally. Exit status 2, even when other names on the same line were listed.
    Fix
    Check where you are with pwd, use an absolute path, and remember that names are case-sensitive.
  2. Symptom: ls: cannot open directory '/root': Permission denied

    Cause
    Listing a directory needs read permission on it. Run as the user nobody, ls /root failed while ls -ld /root worked, because the second only looks up /root itself, which needs search permission on /, not read permission on /root.
    Fix
    Use sudo ls for a system directory, or fix ownership and mode for a folder your app should read. cannot access '/root/.bashrc' for a single file means a directory on the way lacks execute (search) permission.
  3. Symptom: bash: /usr/bin/ls: Argument list too long

    Cause
    ls *.log in a directory with very many files: the shell expands the glob into one command line longer than the kernel accepts. In our test, 25,000 files with long names were enough. Exit status 126 comes from bash, which could not start ls at all. Older systems without merged /usr show /bin/ls, and in a script the line starts with the script name and line number.
    Fix
    any Linux
    find . -maxdepth 1 -name '*.log' | wc -l     # count them
    find . -maxdepth 1 -name '*.log' -mtime +30   # list old ones
  4. Symptom: Plain output, no colors

    Cause
    No alias ls='ls --color=auto' for this user (root on Debian), output going into a pipe, or TERM empty or dumb.
    Fix
    Uncomment the ls lines in ~/.bashrc and open a new shell, or run ls --color=auto directly.

How we tested

OS image
Debian 12.15 (bookworm)
Versions
GNU coreutils 9.1-1GNU bash 5.2.15LANG=C.UTF-8
Tested on
Where
One of our own Debian 12 servers, in a scratch test tree
Method
Every output block was run as shown with ls, find, stat, du and touch in a scratch test tree (plus script for the terminal layout and TERM changes for colors); the test files were created there and removed afterwards
From documentation, not run
BusyBox options (BusyBox 1.35 help text, Debian package), Ubuntu skel aliases (Ubuntu bash package), Debian 13 and Ubuntu 24.04 coreutils versions (packages.debian.org, packages.ubuntu.com), Ubuntu rust-coreutils (Canonical: Ubuntu Discourse, April 22, 2026, Ubuntu Server documentation and the Ubuntu 26.10 release notes), tree options

Frequently asked

How do I show hidden files with ls?

Run ls -a to include every name that starts with a dot, or ls -A to leave out the . and .. entries. ls -la shows them in the long format with permissions and sizes. To list only the hidden entries, run ls -ld .[!.]* ; the -d keeps ls from opening hidden directories such as .git.

What do the columns in ls -l mean?

From left to right: file type and permissions (for example -rwxr-xr-x), the hard link count, the owner, the group, the size in bytes, the last modification time and the name. A symlink adds -> and its target. Files older than six months show the year instead of the time, and the total line at the top is disk space in 1 KiB blocks, not the sum of the sizes.

How do I list only directories with ls?

Run ls -d */ in the directory. The shell expands */ to the directories and -d prints their names instead of their contents; it includes symlinks to directories and skips hidden ones (add .*/ for those). ls -ld */ gives the long format. In scripts, use find . -mindepth 1 -maxdepth 1 -type d instead.

How do I sort ls output by size?

ls -lS sorts the largest file first, ls -lhS does the same with readable sizes such as 140M, and adding -r puts the smallest first. Directories sort by the size of their entry list, usually 4.0K, not by their contents, so use du -sh */ to see which directory is big.

How do I list files recursively in Linux?

ls -R lists the current directory and then every subdirectory under its own heading; ls -lR adds the long format. ls has no depth limit, so for big trees use tree -L 2 (from the tree package) to see the structure, or find, which prints a full path per line and can filter by name, size and age.

Related articles

All guides