---
title: 'A better ls on macOS: use eza, but not as an alias'
description: >-
  Replacing the macOS ls with eza takes one line of zsh - but if you write it as an alias it breaks
  the moment you pass it a directory. Here is the version that...
url: https://expiredqueues.com/better-ls-macos-eza/
date: '2024-04-16T09:00:00.000Z'
image: https://expiredqueues.com/images/og/better-ls-macos-eza-46ae0bda.jpg
---

Developer Tools posts tools

# A better ls on macOS: use eza, but not as an alias

16 April 2024 Updated 8 October 2024 By Adrian 7 min read

I spend most of my day in a terminal, and I don't need `ls` to tell me who owns a file.

Here is the front end of one of my projects, as macOS `ls -l` sees it:

```text
total 640
-rw-r--r--@  1 adrian  staff    6739 Apr 15 09:12 AGENTS.md
drwxr-xr-x@ 14 adrian  staff     448 Apr 16 08:41 app
drwxr-xr-x@ 15 adrian  staff     480 Apr 16 08:41 components
-rw-r--r--@  1 adrian  staff     382 Apr 15 09:12 eslint.config.mjs
drwxr-xr-x@  5 adrian  staff     160 Apr 16 08:41 lib
-rw-r--r--@  1 adrian  staff     251 Apr 15 09:12 next-env.d.ts
-rw-r--r--@  1 adrian  staff     129 Apr 14 22:07 next.config.ts
drwxr-xr-x@ 16 adrian  staff     512 Apr 15 09:12 node_modules
-rw-r--r--@  1 adrian  staff     660 Apr 15 09:12 package.json
-rw-r--r--@  1 adrian  staff  132858 Apr 16 16:14 pnpm-lock.yaml
-rw-r--r--@  1 adrian  staff     146 Apr 14 22:07 postcss.config.mjs
drwxr-xr-x@  3 adrian  staff      96 Apr 14 22:07 public
-rw-r--r--@  1 adrian  staff     275 Apr 16 23:16 README.md
drwxr-xr-x@  4 adrian  staff     128 Apr 15 09:12 src
-rw-r--r--@  1 adrian  staff     698 Apr 14 22:07 tsconfig.json
-rw-r--r--@  1 adrian  staff  155028 Apr 16 08:41 tsconfig.tsbuildinfo
```

Useful information, certainly. Four columns of it before you reach a single filename. But most of the time I am not looking at a Unix filesystem because I need to audit permissions. I am looking for a file.

I would rather see this:

![A zsh session: ls listing two directories, then ls website listing the same project with a folder or file-type icon in front of every name.](https://expiredqueues.com/images/opt/eza-ls-directory-argument-320.jpeg)

The same directory, and the one above it. Directories first, icons by file type, one entry per line.

Same directory, same information I actually wanted, and I can find `package.json` in it without reading a single permission bit.

That is what this setup does. We replace `ls` in the interactive shell with [eza](https://github.com/eza-community/eza), give it icons and colours, and - the part most write-ups get wrong - make sure it still behaves when you pass it arguments.

## Why eza

There are plenty of ways to customise a terminal. I am not interested in turning mine into a dashboard. eza is the maintained fork of the old `exa`, and the useful parts of it are simple:

- sensible file listings
- directories grouped first
- optional icons
- file-type colours
- tree views
- useful sorting options
- Git integration
- familiar `ls`-style arguments

The important bit is the last one. It still feels like `ls`.

## 1. Install a Nerd Font

If you want icons, your terminal needs a font that carries the Nerd Font glyphs. Check what you already have:

```bash
system_profiler SPFontsDataType | grep -i "nerd"
```

If nothing comes back, Fira Code is a reasonable choice:

```bash
brew install --cask font-fira-code-nerd-font
```

### Make sure your terminal is actually using it

This is the part that is easy to miss. Your editor font and your terminal font are two different settings. In VS Code:

```json
{
  "editor.fontFamily": "Fira Code",
  "terminal.integrated.fontFamily": "FiraCode Nerd Font",
  "terminal.integrated.lineHeight": 1.2
}
```

You do not need to restart VS Code - run **Developer: Reload Window** from the Command Palette.

In [iTerm2](https://expiredqueues.com/iterm2-mac-natural-text/) the setting lives under **Settings → Profiles → Text → Font**; pick **FiraCode Nerd Font** there.

## 2. Install eza

```bash
which eza || brew install eza
```

Check that it runs:

```bash
eza -1 --group-directories-first
```

At that point you already have a better `ls`. The `-1` matters: it puts each entry on its own line instead of eza's default grid.

## 3. Add a theme

**Added October 2024:** eza 0.20 reads a theme file, which earlier versions did not. If you are on an older build, skip this section - everything else still works.

Create the config directory:

```bash
mkdir -p ~/.config/eza
```

I use Tokyo Night:

```bash
curl -L https://raw.githubusercontent.com/eza-community/eza-themes/refs/heads/main/themes/tokyonight.yml \
  -o ~/.config/eza/theme.yml
```

Then tell eza where to look, in `~/.zshrc`:

```bash
export EZA_CONFIG_DIR="$HOME/.config/eza"
```

Reload and confirm:

```bash
source ~/.zshrc
echo $EZA_CONFIG_DIR
```

## 4. Don't alias this one

Here is the bit worth the post. You will be tempted to write:

```bash
alias ls='eza -1 --icons=always --color=always --group-directories-first | sed "s/^/ › /"'
```

Don't. It looks fine until you do the thing in the screenshot above:

```bash
ls website
```

A shell alias is a textual substitution, so your command becomes:

```bash
eza -1 --icons=always --color=always --group-directories-first | sed "s/^/ › /" website
```

`website` now belongs to `sed`, not to eza. Once `sed` has a file operand it stops reading `stdin`, so eza's listing is thrown away and you get whatever sed makes of `website` instead - nothing at all for a directory, or the entire contents of the file if you happened to name one:

```text
 › {
 ›   "name": "@pwtray/website",
 ›   "version": "1.0.1",
```

`ls -la` is worse only in that it tells you:

```text
sed: -la: No such file or directory
```

Same story for every argument you type after the command name.

Use a shell function instead:

```bash
unalias ls 2>/dev/null
function ls { eza -1 --icons=always --color=always --group-directories-first "$@" | sed 's/^/ › /'; }
```

The whole fix is `"$@"`. It puts your arguments where they belong - on eza, before the pipe. So `ls`, `ls website`, `ls -a`, and `ls website/app -T -L 2` all do what you expect.

## 5. Put it at the end of `.zshrc`

This matters if you use Oh My Zsh or another framework: define the function **after** the framework loads, or the framework's own `ls` alias wins.

```bash
# plugins, framework, PATH, etc.

export EZA_CONFIG_DIR="$HOME/.config/eza"

unalias ls 2>/dev/null
function ls { eza -1 --icons=always --color=always --group-directories-first "$@" | sed 's/^/ › /'; }
```

**Why the `unalias`?** If `ls` is already an alias (oh-my-zsh defines one, for example), zsh substitutes the alias text into your function definition line before running it. The result is garbage syntax, and you get:

```bash
parse error near `()'
```

Removing the alias with `unalias ls 2>/dev/null` first means the next function line defines a clean function named `ls`. The `function` keyword adds a safety net: unlike the `ls() { ... }` syntax, the name after `function` is never alias-expanded.

```bash
source ~/.zshrc
```

## 6. Try it

```bash
ls
```

Here, the `ls` gives you the listing from the top of the post - with icons in front of each name, if your terminal font has them. `ls website` gives you the same treatment one directory down, which is the whole reason for the function.

The rest of eza is still there too:

```bash
ls -a           # hidden files
ls -T -L 2      # a two-level tree
ls -s modified  # sorted by modification time
ls -D           # directories only
ls | grep .md   # and it still pipes
```

Flags and paths in the same command work the way they always did:

![ls website/app -T -L 2 drawing a two-level tree of a Next.js app directory, with connector lines and file-type icons.](https://expiredqueues.com/images/opt/eza-ls-tree-view-320.jpeg)

`ls website/app -T -L 2` - a path and two eza flags, passed straight through by `"$@"`.

## Keeping the real `ls`

There are still times I want the traditional output, and it never went anywhere:

```bash
command ls -la
\ls -la
```

The function only exists in your interactive shell. It does not touch `/bin/ls`, and scripts running outside that shell never see it. That is exactly how this kind of customisation should work: convenient at the prompt, invisible everywhere else.

## One detail: `--color=always`

Command-line tools are normally careful about colour. If stdout is a terminal they emit colour codes; if stdout is a pipe they usually turn colour off, so the escape sequences don't end up inside whatever is reading them.

Our function deliberately pipes eza through `sed`:

```text
eza → sed → terminal
```

So eza does not see a terminal. It sees a pipe, and it would drop both the colours and the icons. That is why `--color=always` and `--icons=always` are in there: they force the decoration back on.

The trade-off is that the escape codes are now real characters in the output. Anything that understands ANSI is fine:

```bash
ls | less -R
```

Anything that doesn't will show you the codes, so use `command ls` when you are feeding the output to something that parses it.

## The final setup

```bash
export EZA_CONFIG_DIR="$HOME/.config/eza"

unalias ls 2>/dev/null
function ls { eza -1 --icons=always --color=always --group-directories-first "$@" | sed 's/^/ › /'; }
```

That is the lot. No replaced system binaries, no changes to macOS, no wrapper script sitting in `/usr/local/bin`. Just a small function that makes the command I run a few hundred times a day show me what I actually came to see.

### You might be interested in:

- [Git commands I wish I knew early](https://expiredqueues.com/git-commands-i-wish-i-knew-early/) - the other half of the same terminal.
- [The Pragmatic Programmer](https://expiredqueues.com/books/the-pragmatic-programmer/) - where the case for small, local, invisible shell automation comes from.

[Previously Stop scrolling the Network panel: filter it](https://expiredqueues.com/filter-chrome-devtools-network-requests/) [Next Home network setup that just stays quiet](https://expiredqueues.com/home-network-setup/)

## Structured data

```json
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "A better ls on macOS: use eza, but not as an alias",
  "datePublished": "2024-04-16T09:00:00.000Z",
  "dateModified": "2024-10-08T09:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "Adrian",
    "url": "https://expiredqueues.com/about/"
  },
  "publisher": {
    "@type": "Person",
    "name": "Adrian"
  },
  "mainEntityOfPage": "https://expiredqueues.com/better-ls-macos-eza/",
  "image": "https://expiredqueues.com/images/2024/eza-ls-directory-argument.png",
  "keywords": "posts, tools, macos, terminal, tips-tricks",
  "articleSection": "Developer Tools"
}
```
