Short answer
lslists the files in the current directory,ls /var/logthe ones in another directory.ls -laadds hidden files (names that start with a dot) and shows permissions, owner, size and date for each entry.ls -lhprints sizes as 4.0K, 25M or 1.1G instead of bytes; addS(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
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 directoryEvery 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
$ 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 uploadsTake one line apart, the one for the shell script:
-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| Column | Example | What it tells you |
|---|---|---|
| File type | - d l c b p s | Regular file, directory, symbolic link, character device, block device, named pipe, socket. |
| Permissions | rwxr-xr-x | Read, 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 character | rw-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. |
| Links | 3 | Hard 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, group | root root | Names 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. |
| Size | 18 | Bytes. 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. |
| Time | Nov 2 02:30 | Last 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:
$ 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.logTwo 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:
$ 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 .htaccessls -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-dmatters: without it, ls would print the contents of.gitinstead of the line for.gititself. Since bash 5.2 the simplerls -d .*also works, because the new defaultglobskipdotsno 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
$ 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.txtBiggest 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:
$ 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
$ 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:
$ 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.htmlstat 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
$ 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-vsorts numbers inside names naturally, sobackup-10comes afterbackup-9.-Xgroups by extension (everything without one first, then.gz,.html,.sh,.txt).--group-directories-firstkeeps directories (and symlinks to them) at the top and still sorts each group by the other options.-Udoes 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
$ 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
$ 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
logsThree answers, and they disagree on purpose:
ls -d */: the shell expands*/to every directory, and-dtells ls to print those names instead of opening them. It includescurrent, a symlink that points to a directory, and skips hidden directories. Add.*/(ls -d */ .*/) to include them, or usels -ld */for the long format.ls -p | grep /:-pappends 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 throughsortif 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
$ 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.jpgls -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 -R | tree | find | |
|---|---|---|---|
| Installed on Debian and Ubuntu by default | Yes | No | Yes |
| Full path on every line | No | -f | Yes |
| Limit the depth | No | -L 2 | -maxdepth 2 |
| Filter by name, size or age | No | name only (-P) | Yes |
| NUL-separated names for scripts | No | No | -print0 |
| Best for | a quick look | showing structure | anything you act on |
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
$ 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 directoryls 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/*.dprints what is inside every.ddirectory;-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 *.logonly looks in the current directory. For a whole tree, usefind . -name '*.log'; with many thousands of matches, see Argument list too long below.
Useful ls options by task
| Task | Command | Note |
|---|---|---|
| One name per line | ls -1 | Already the default when output goes into a pipe or a file |
| Columns, sorted down | ls -C | The default in a terminal; forces columns into a pipe too |
| Columns, sorted across | ls -x | Same as --format=across; other values: commas, long, single-column, vertical |
| Set the line width | ls -x -w 40 | Overrides the terminal width and COLUMNS; -w 0 means no limit |
| Files matching a pattern | ls -l *.conf | The shell expands the pattern, see by pattern |
| Sort by creation time | ls -lt --time=birth | GNU coreutils 8.32 and later, on filesystems that record it |
| Mark type after the name | ls -F | / directory, @ symlink, * executable |
| Show inode numbers | ls -li | Same number on two names = hard links to one file |
| The directory itself, not its contents | ls -ld /var/www | Check permissions of a folder |
| Follow a symlink to a directory | ls -l current/ | The trailing slash (or -L) lists the target |
| Full timestamp | ls -l --full-time | Seconds, nanoseconds and time zone |
| ISO dates | ls -l --time-style=long-iso | Prints 2025-03-14 08:00 for every line |
| Custom date format | ls -l --time-style=+'%F %T' | Any date(1) format after the + |
| Numeric owner and group | ls -n | UID and GID instead of names |
| Hide a pattern | ls -I '*.log' | --hide does the same but gives way to -a |
| Hide editor backups | ls -B | Leaves out names that end in ~, such as run.sh~ |
| SELinux context | ls -lZ | Adds the security context; prints ? where none is set, as on a stock Debian server |
| Comma-separated | ls -m | Fills the line, handy for pasting |
| Names in double quotes | ls -Q | Or ls -b to escape spaces and newlines |
| Unsorted, fastest | ls -U | Directory order |
| NUL-separated names | ls --zero | GNU coreutils 9.0 and later only |
| Help and version | ls --help | man 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:
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 outRoot 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:
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=autoonly colors a terminal. Pipe ls intolessorgrepand the colors disappear;ls --color=always | less -Rkeeps them. The default palette fromdircolorsis bold blue for directories, bold cyan for symlinks and bold green for executables;~/.dircolorsoverrides it.- No colors even with --color=always? Since coreutils 8.23, with neither
LS_COLORSnorCOLORTERMset, ls honors an empty or unknownTERM. WithTERM=dumbour 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 -Norexport QUOTING_STYLE=literalturns it off. By default, pipes and files get no quotes;-Q,-b,--quoting-styleor an exportedQUOTING_STYLEquote there too.
$ 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 uploadsWhy 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:
$ for f in $(ls); do echo "[$f]"; done
[backup.tar.gz]
...
[my]
[notes.txt]
...
$ touch $'bad\nname.txt'
$ ls | wc -l
10The 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:
$ 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.txtWhat to use instead, each tested on the same tree:
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 scriptstat 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 -v | Yes | Yes |
--full-time, --group-directories-first | Yes | Yes |
--color | Yes | Yes |
--time-style | Yes | No |
-I / --hide | Yes | No |
-Q, -m, -N, -b | Yes | No |
--block-size, --si | Yes | No |
-g, -o, -U | Yes | No |
--zero | 9.0 and later | No |
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
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
*.logthat 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.
Symptom:
ls: cannot open directory '/root': Permission denied- Cause
- Listing a directory needs read permission on it. Run as the user
nobody,ls /rootfailed whilels -ld /rootworked, because the second only looks up/rootitself, which needs search permission on/, not read permission on/root. - Fix
- Use
sudo lsfor 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.
Symptom:
bash: /usr/bin/ls: Argument list too long- Cause
ls *.login 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/usrshow/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
Symptom: Plain output, no colors
- Cause
- No
alias ls='ls --color=auto'for this user (root on Debian), output going into a pipe, orTERMempty ordumb. - Fix
- Uncomment the ls lines in
~/.bashrcand open a new shell, or runls --color=autodirectly.
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.