# Customize your shell with ZSH A series of 7 article(s), in reading order. # Customize your Linux prompt with Powerlevel10k > Test and customize the Powerlevel10k Zsh theme for your Linux or WSL prompt in a safe Docker sandbox environment before committing to a full installation. --- ![Customize your Linux prompt with Powerlevel10k](/img/v2/customization_prompt.webp) **TL;DR** This article shows how to try the Powerlevel10k Zsh prompt theme risk-free using a disposable Alpine Docker container (a one-liner from the official README): everything — git, zsh, nano, the theme, and its config wizard — runs entirely in RAM and disappears on `exit`, letting you decide whether to install it for real before touching your actual machine. When you're working with Linux (also working with WSL thus), there are many ways to personalize your prompt. One of the simplest solutions is to use [Powerlevel10k](https://github.com/romkatv/powerlevel10k) and its wizard. In this article we're going to use a Docker container just to: *test and discard*. You'll see exactly what the prompt looks like, play with the wizard, and leave your own machine untouched. The tip comes from [https://github.com/romkatv/powerlevel10k/blob/master/README.md](https://github.com/romkatv/powerlevel10k/blob/master/README.md#try-it-in-docker) ## What Powerlevel10k looks like Here is my own prompt, once the wizard has run: ![A Powerlevel10k prompt](./images/p10k-prompt.webp) Read it from left to right: the current folder, then the git branch (`main`) with `?1` telling me one file is modified and not committed, then — on the right side — a green check for the previous command, the `root@AVONTURE-RACOUR` identity I'm connected as, and the time the command finished. All of that, permanently, without typing `git status` or `whoami`. That's the pitch; the rest of this article is about trying it without touching your machine. ## Try it without installing anything By running the single command below, you'll download a very small Linux Alpine image then start some initializations like installing `git`, `nano`, `zsh`, ... The Powerlevel10k repository will be downloaded from Github and its wizard will be started. ```bash $ docker run -e TERM -e COLORTERM -e LC_ALL=C.UTF-8 -it --rm alpine sh -uec ' apk add git zsh nano vim git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ~/powerlevel10k echo "source ~/powerlevel10k/powerlevel10k.zsh-theme" >>~/.zshrc cd ~/powerlevel10k exec zsh' ``` Answer the wizard's questions, play with the result, and decide if you want to adopt it or not. > **Everything is done in RAM; nothing on your disk:** > > Running the `docker run` command here above will download a Docker Alpine Linux image on your disk (less than 7 MB) then will install binaries inside the running container so, by leaving the container using the `exit` command, nothing will stay on your disk. Ideal for testing. ## Why I kept it * When an instruction is finished, the new prompt displays the time taken by the instruction, useful when you're trying to optimize a command, * On the right, you can see immediately if the instruction has failed, with a red display and the error code (`exitcode`), * It integrates nicely with [Windows Terminal](/blog/windows-terminal) if you're on WSL, and with the modular setup described in [Beyond the Monolith - Organizing Your ZSH Workflow Like a Pro](/blog/modular-zsh-workflow), * And, of course, the visual aspect, which is pretty cool. And also, because I work in Docker containers on a daily basis, using Powerlevel10k locally gives me a strong visual indication to remind me at all times whether I'm local or in a container. ## Conclusion The nice thing about this sandbox is that the decision costs you nothing: you type one command, you look at the prompt for two minutes, and you type `exit`. Whatever you answered to the wizard disappears with the container. If you liked what you saw, do it for real: [How to install Oh-My-ZSH](/blog/zsh-install) covers the on-disk installation (Oh-My-Zsh first, then Powerlevel10k), and the official [installation guide](https://github.com/romkatv/powerlevel10k#installation) has the details for every other setup. --- # How to install Oh-My-ZSH > Install Zsh and Oh-My-ZSH easily! Learn to power up your Linux terminal with the Powerlevel10k theme, custom functions like take, and essential daily-use features. --- ![How to install Oh-My-ZSH](/img/v2/zsh.webp) **TL;DR** This article walks through installing Zsh and Oh My Zsh on Linux, then upgrading the prompt with the Powerlevel10k theme and trimming its default username/hostname display. It also highlights a handful of daily-use features worth knowing: the `take` function to create and enter a folder in one step, `cd ...` chains to jump up multiple parent directories, `TAB`-driven autocompletion, and Git aliases like `gst` and `gwip`. ZSH is a powerful alternative to Linux Bash offering a lot of features like auto-completion (I like this so much), plugins and even themes. *Once installed, two articles take it further: [Beyond the Monolith - Organizing Your ZSH Workflow Like a Pro](/blog/modular-zsh-workflow) to keep your `.zshrc` from becoming a monster, and [ZSH Functions - Customizing Your Shell for Docker Management](/blog/zsh-docker-functions).* The idea here is to empower your Linux console: improve the command line (f.i. new aliases out-of-the-box) and make the look and feel even better. I've been using [Oh My ZSH](https://ohmyz.sh/) for years; let's see how to install it, followed by discovering some features. ## Where We're Going This is my prompt once Zsh, Oh-My-Zsh and the Powerlevel10k theme are in place: ![Powerlevel10k - Prompt without username](./images/powerlevel10k_prompt_no_user.webp) At a glance I know I'm in my `blog` folder, that it's a Git repository, that I'm on branch `main` and that `?1` means one *untracked* or *modified* file is waiting on my computer, not yet pushed to GitHub. No command typed, no `git status` run: the information is simply there, refreshed at every prompt. ## Why it's worth the ten minutes * **Oh-My-Zsh is a framework, not a theme.** It ships hundreds of aliases and functions out of the box, so you get value before configuring anything. * **The prompt becomes a dashboard.** Folder, repository, branch and pending-changes count are permanently displayed, which is exactly the state you keep checking manually. * **Autocompletion is on another level.** `TAB` lists sub-folders, command flags and Git branches, instead of just completing a file name. ## Seeing It in Action with Docker Before touching your `~/.zshrc`, spin up a throwaway container and play with Oh-My-Zsh for real. > **Why Docker first?:** > > A container lets you feel what Oh-My-Zsh changes — the prompt, the aliases, the daily-use tricks below — without switching your login shell or editing a single file on your machine. Don't like it? Remove the container, nothing lingers. Here is the Dockerfile I prepared. It installs Oh-My-Zsh unattended and also pre-builds a small playground: a deep folder tree to try `cd ...` on, and a git repository with a commit and an untracked file so `gst` has something to show: **Dockerfile** ```docker FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ zsh \ git \ curl \ ca-certificates && \ rm -rf /var/lib/apt/lists/* # Install Oh-My-Zsh unattended: no interactive prompt, no shell switch, keep # the default .zshrc so it can be inspected/edited straight away. RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" \ "" --unattended --keep-zshrc # Pre-build a demo playground so the daily-use features have something real # to act on: a deep folder tree (for `cd ...`) inside a git repo with a # commit and an untracked file (for `gst`, the `git status` alias). RUN git config --global user.email "demo@example.com" && \ git config --global user.name "Demo" && \ mkdir -p /root/demo/a/b/c/d/e/f && \ cd /root/demo && \ git init -q -b main && \ echo "# Demo repo" > README.md && \ git add README.md && \ git commit -q -m "Initial commit" && \ echo "draft notes, not committed" > notes.txt WORKDIR /root/demo/a/b/c/d/e/f # Signal clearly that this shell runs inside a container (overrides the # Oh-My-Zsh theme prompt on purpose) RUN echo "PROMPT='%B%F{yellow}🐳 %F{cyan}%n%f%b %~ %# '" >> /root/.zshrc CMD ["/bin/zsh", "-i", "-l"] ``` Build and run it: ```bash $ docker build -t zsh-demo . [+] Building 22.1s (8/8) FINISHED ✔ exporting to image $ docker run --rm -it zsh-demo 🐳 root ~/demo/a/b/c/d/e/f # ``` You land directly inside `/root/demo/a/b/c/d/e/f` — six folders deep, git repo already initialized. Try the features from the next section right away: ```bash $ cd ...... 🐳 root ~/demo/a # $ gst On branch main Untracked files: notes.txt $ take /tmp/scratch 🐳 root /tmp/scratch # ``` `cd ......` (six dots) jumps up five parents in one go. `gst` shows the untracked `notes.txt` without you typing `git status` — it works from any depth inside the repo, since Git walks up to find `.git` on its own. `take` creates `/tmp/scratch` and drops you inside it. Press `TAB` after `cd ` to see folder autocompletion in action. ## Installation The installation is quite easy, just three commands: ```bash $ sudo apt-get update && sudo apt-get install zsh $ chsh -s /usr/bin/zsh $ sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" ``` With the last instruction, you'll be asked if you want to change the default shell to zsh; simply answer `Y`es. And very quickly you can see a change in your console: ![ZSH has been installed](./images/zsh_install.webp) The first change concerns the prompt. Right now, I'm in my blog folder and it's a git repository. So, Oh-My-ZSH is showing me that info and, on top, the current branch I'm working on (branch `master` here). Nice but, we can do much better. Time to install a template engine called Powerlevel10k (). If you'd rather try it in a throwaway container before touching your machine, read [Customize your Linux prompt with Powerlevel 10k](/blog/powerlevel10k_sandbox). In the next paragraph, I'll install Powerlevel10k on my computer, so let's go ahead. ### Powerlevel10k Installation is easy too, just run `git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k` in your console. You'll then download the template. The second action to take is to edit the `~/.zshrc` file. Since I'm using VSCode, I just start VSCode, press `CTRL`+`O` and open the `~/.zshrc` file. Search for `ZSH_THEME` and set the value to `powerlevel10k/powerlevel10k`. ![ZSH theme](./images/zsh_theme.webp) The last thing is to close your current Linux console and start a new one. The first time, you'll get the Powerlevel10k configuration wizard: ![ZSH configuration](./images/zsh_configuration.webp) Just answer all the questions and when it's time to define your preference, just select the option you like the most. ![ZSH - Set your preferences](./images/zsh_preferences.webp) When done, here is what my prompt will look like: ![Powerlevel10k - New prompt](./images/powerlevel10k_prompt.webp) And this is so much better (and much prettier). Quickly, I know I'm in my `blog` folder, that it's a Git repository, that I'm on branch `main` and `?1` means I've one *untracked* or *modified* file on my computer and not yet pushed to the central repo (GitHub here). At the right, we can see I'm connected as `root` on my machine. It's shown in red just to indicate I have root access. #### Customizing the prompt Ok, let's remove that part since, yeah, it's my home computer, it's normal here I'm root since I've just one local account and that information is then meaningless to me. Powerlevel10k has great documentation, and by reading [How do I add username and/or hostname to prompt?](https://github.com/romkatv/powerlevel10k?tab=readme-ov-file#how-do-i-add-username-andor-hostname-to-prompt), we learn how to remove it the same way. Just edit the `~/.p10k.zsh` file, search for `POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS` and then find its `context` entry and comment out the line. Save the file, open a new console and bingo, the right side of the prompt no longer contains the username — you're now looking at the prompt shown at the top of this article. ## Some features I use daily You might think these features are not needed but the fact is I'm using them so much in my daily work. For instance, `take /tmp/new_folder` will create the folder if not yet present and will jump in it. Be honest, `take /tmp/new_folder` is easier and faster than `mkdir -p /tmp/new_folder && cd $_` no? > **Take is just a custom function:** > > `take` is, in fact, a function defined in file `~/.oh-my-zsh/lib/functions.zsh`. A second, so stupid but so valuable feature is `cd ..` followed by two or three or ... dots. Imagine you're in the folder `/tmp/new_folder/a/b/c/d/e/f` and you want to use `cd ..` to jump back to parent `a`. Using ZSH, I can just type `cd ......` and that's cool. `cd ..` jumps to the first parent, `cd ...` to the grandparent and so on. There is also one key feature with ZSH and it's `TAB`. Imagine you're in a folder having multiple sub-folders. Just type `cd` (the space is really important) and press `TAB`. You'll then get the list of sub-folders. Navigate between suggestions by using `TAB` and press `ENTER` to select one. ![ZSH - CD with tab](./images/zsh_cd.webp) Then, continue, press `TAB` again and you'll be able to select a sub-folder and you can continue like that. ![ZSH - CD with tab](./images/zsh_cd_again.webp) `TAB` is then used by ZSH for auto-completion. Let's try something else: `head -` followed by `TAB`. As you can see below, I'll get the list of command line flags I can use, I can navigate still using `TAB` and continue to type my command line. ![ZSH - head command auto-completion](./images/zsh_head_autocompletion.webp) In terms of aliases, one I use several times a day is `gst` for `git status`. You can retrieve the list of all aliases for Git in your `/root/.oh-my-zsh/plugins/git/git.plugin.zsh` file. A nice one is `gwip` to commit all your changes as `work in progress` allowing you to store your current work, checkout another branch (f.i. to work on an issue) and when done, checkout back your feature branch and retrieve your work. Easy. And for sure, there are more — I probably don't even know they're part of ZSH anymore. ## Conclusion Three commands install Zsh and Oh-My-Zsh, a `git clone` and one `ZSH_THEME` line bring the Powerlevel10k prompt, and a single commented-out entry in `~/.p10k.zsh` removes the noise you don't need. From there the gains are permanent: `take`, `cd ...`, `TAB` completion and `gst` are typed dozens of times a day without you thinking about them. There is much, much more, like [plugins](/blog/tags/zsh). Next up, [zsh-autosuggestions](/blog/zsh-plugin-autosuggestions) and [zsh-syntax-highlighting](/blog/zsh-syntax-highlighting), two must-haves once Oh-My-Zsh is in place. --- # Autosuggestions in the console using ZSH > Boost your ZSH console efficiency. Install the zsh-autosuggestions plugin easily and get intelligent command suggestions from your history as you type. --- ![Autosuggestions in the console using ZSH](/img/v2/zsh.webp) **TL;DR** This article covers `zsh-autosuggestions`, a Zsh plugin that suggests commands from your shell history as you type, shown in gray text you can accept with `TAB`. It explains how to install the plugin by cloning it into your Oh My Zsh custom plugins folder and enabling it in `~/.zshrc`, then shows how to accept, cycle through, or ignore suggestions using `TAB` and the arrow keys. ZSH supports plugins, and one of the wonders is the [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions) one — assuming you've already installed Oh-My-Zsh (see [How to install Oh-My-ZSH](/blog/zsh-install) if not). That one will suggest commands as you type, based on your previous history and completions. Three keystrokes, and the forty-character command you painfully assembled last month is sitting there, waiting for a `TAB`. ## Three Characters, and the Command Is Already There Imagine you've already typed, today, yesterday or weeks ago, the `docker compose up --detach` command. Today, you type `doc` and here is what your console shows: ![Autosuggestions plugin for ZSH](./images/autosuggestions.webp) The gray part is the suggestion. Press `TAB` and the whole line is yours; keep typing and the suggestion updates at every character; ignore it and nothing happens. You can also play with `UP` and `DOWN` to switch to other commands matching the same keys (*doc* in my example). ## Why it works * Suggestions come straight from your Linux HISTORY: no index to build, no configuration, nothing to teach the plugin. * The more you use your console, the better the suggestions get — it learns from you and only from you. * No more wondering, "Gee, what were the parameters I used for ...". ## Seeing It in Action with Docker You don't need Oh-My-Zsh already running on your machine to see the plugin work — a throwaway container does the job. > **Why Docker first?:** > > The Dockerfile below installs Oh-My-Zsh, clones the plugin, enables it in `~/.zshrc`, and — the > part that matters — pre-seeds a shell history containing `docker compose up --detach`. Type `doc` > and the exact scenario from the screenshot above appears, with nothing to configure yourself. **Dockerfile** ```docker FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ zsh \ git \ curl \ ca-certificates && \ rm -rf /var/lib/apt/lists/* # Install Oh-My-Zsh unattended (host framework required by the plugin) RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" \ "" --unattended --keep-zshrc # Install zsh-autosuggestions and enable it in the default plugins=() list RUN git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions \ /root/.oh-my-zsh/custom/plugins/zsh-autosuggestions && \ sed -i 's/^plugins=(git)/plugins=(git zsh-autosuggestions)/' /root/.zshrc # Pre-seed a shell history so a suggestion is already there to accept: type # "doc" and the full `docker compose up --detach` line appears in gray text. RUN printf '%s\n' \ ': 1700000000:0;docker compose up --detach' \ ': 1700000100:0;docker compose logs --follow' \ ': 1700000200:0;docker compose down --volumes' \ > /root/.zsh_history # Signal clearly that this shell runs inside a container RUN echo "PROMPT='%B%F{yellow}🐳 %F{cyan}%n%f%b %~ %# '" >> /root/.zshrc CMD ["/bin/zsh", "-i", "-l"] ``` Build and run it: ```bash $ docker build -t autosuggestions-demo . [+] Building 19.8s (8/8) FINISHED ✔ exporting to image $ docker run --rm -it autosuggestions-demo 🐳 root ~ # ``` Now just type `doc` (don't press `ENTER` yet) and the exact scene from the screenshot at the top of this article plays out: `docker compose up --detach` fills in as gray, ghosted text right after your cursor — read straight from the pre-seeded history. Press `TAB` to accept it, or `↑`/`↓` to cycle to the other two `docker compose` commands also sitting in that history. ## Installation Just clone the official repository like below: ```bash $ `git clone https://github.com/zsh-users/zsh-autosuggestions \${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions` ``` Then edit the `~/.zshrc` file, search for `plugins=(` and add `zsh-autosuggestions` to the list. You'll have f.i. something like: **\~/.zshrc** ```bash plugins=( git # ... your existing plugins zsh-autosuggestions ) ``` Close your console, open a new one (or run `source ~/.zshrc` to load your change) and it's done. ## Conclusion One `git clone`, one line in `~/.zshrc`, and your terminal stops asking you to remember flags you typed three weeks ago. Because everything is read from your history, the plugin is worth more every day you use it. Since suggestions are only as good as what's stored, it's worth knowing how that history is kept and filtered: see [Linux - Working with the history of your last fired actions](/blog/linux-history). And for a plugin in the same spirit, dedicated to your SSH hosts: [SSH - Autosuggestions with ZSH](/blog/zsh-plugin-ssh-config-suggestions). --- # Syntax highlighting in the console using ZSH > Enhance your Zsh console with syntax highlighting! See commands turn green (valid) or red (invalid) as you type. Quick installation guide for zsh-syntax-highlighting. --- ![Syntax highlighting in the console using ZSH](/img/v2/zsh.webp) **TL;DR** This article introduces `zsh-syntax-highlighting`, a Zsh plugin that colors commands in your terminal as you type: green for valid, executable commands and red for typos or unknown ones. It walks through cloning the plugin into your Oh My Zsh custom plugins folder and adding it to the `plugins=(...)` list in `~/.zshrc`, with no further configuration needed to start using it. [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting/) is another gem for ZSH — assuming you've already installed Oh-My-Zsh (see [How to install Oh-My-ZSH](/blog/zsh-install) if not). As you type, you'll be able to tell from the colors that, for example, something isn't quite right. If you type `head` followed by a space, ZSH will display this word in green: this command exists and is valid. If you type `heat` there, the word will appear in red: this command does not exist. It sounds simple, but it's so practical. ## What Syntax Highlighting Looks Like Type a command like `cat` or `head` and it turns green: the command exists, it's correctly typed and it's executable. ![Highlight in green](./images/head.webp) Make a typo and the very same word turns red before you even press `ENTER`: ![Highlight in red](./images/docker_compose.webp) That color appears **while you type**, not after the command has failed. You know you've mistyped `docekr` before losing a second to a `command not found`. ## How to use it In fact, nothing has to be done: there is no configuration file, no option to set, no alias to define. Install the plugin, reload your shell, and the coloring is simply there from the next character you type. ## Seeing It in Action with Docker No history to seed, no config to write — this plugin needs nothing but itself, which makes it a perfect one-command Docker test. > **Why Docker first?:** > > The Dockerfile below installs Oh-My-Zsh and the plugin, and enables it in `~/.zshrc` — the exact > two steps from the "Installation" section, done for you. Nothing on your own machine changes. **Dockerfile** ```docker FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ zsh \ git \ curl \ ca-certificates && \ rm -rf /var/lib/apt/lists/* # Install Oh-My-Zsh unattended (host framework required by the plugin) RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" \ "" --unattended --keep-zshrc # Install zsh-syntax-highlighting and enable it in the default plugins=() list RUN git clone --depth=1 https://github.com/zsh-users/zsh-syntax-highlighting \ /root/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting && \ sed -i 's/^plugins=(git)/plugins=(git zsh-syntax-highlighting)/' /root/.zshrc # Signal clearly that this shell runs inside a container RUN echo "PROMPT='%B%F{yellow}🐳 %F{cyan}%n%f%b %~ %# '" >> /root/.zshrc CMD ["/bin/zsh", "-i", "-l"] ``` Build and run it: ```bash $ docker build -t syntax-demo . [+] Building 18.4s (7/7) FINISHED ✔ exporting to image $ docker run --rm -it syntax-demo 🐳 root ~ # head ``` Type `head` and the word turns green as you finish typing it — the command exists. Backspace it, type `heat` or `docekr` instead, and it turns red before you even press `ENTER`: same scene as the two screenshots above, live in your own terminal. ## Installation Just clone the official repository like below: ```bash $ `git clone https://github.com/zsh-users/zsh-syntax-highlighting \${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting` ``` Then edit the `~/.zshrc` file, search for `plugins=(` and add `zsh-syntax-highlighting` to the list. You'll have f.i. something like: **\~/.zshrc** ```bash plugins=( git # ... your existing plugins zsh-syntax-highlighting ) ``` Close your console, open a new one (or run `source ~/.zshrc` to load your change) and it's done. ## Conclusion Two lines of setup and your terminal starts proofreading you: green means "this will run", red means "fix me first". It's the kind of tiny feedback loop you stop noticing after a week — until you sit at a machine without it and start typing blind again. Its natural companion is [Autosuggestions in the console using ZSH](/blog/zsh-plugin-autosuggestions): this one tells you whether what you're typing is valid, the other saves you from typing it at all. --- # Linux - Working with the history of your last fired actions > Master your Linux command line history. Learn how to view, locate (.bash_history, .zsh_history), and manage your previously executed commands with essential tips. --- ![Linux - Working with the history of your last fired actions](/img/v2/linux_tips.webp) **TL;DR** This article covers working with shell history in Bash/ZSH: locating the history file via `$HISTFILE`, excluding sensitive commands with `HISTIGNORE`/`HISTORY_IGNORE` or a leading space (`hist_ignore_space`), filtering with `history | grep`, fuzzy-searching via `CTRL`+`R`, using zsh-autosuggestions to sidestep manual history search entirely, and deleting specific entries with `history -d`. For me, one of the undeniable strengths of the command line under Linux is the management of the history of previously executed commands. *The single biggest upgrade you can make to that history is [fzf](/blog/linux-fzf-introduction): it turns `CTRL`+`R` into a fuzzy, interactive search instead of a linear one. If you want something more structured — with timestamps, exit codes, working directory, and optional multi-machine sync — [Atuin](/blog/atuin-bash-history) replaces the flat history file with a SQLite database entirely.* To be able to press the `Up` or `Down` keys on the keyboard to return to previously run commands, wow. For older users, there was [DOSKEY](https://en.wikipedia.org/wiki/DOSKEY) under MSDos, but Linux goes much further. Let's look at a few tricks. ## Getting your history First things first, to display the list of commands you've entered in the past, simply start `history` in a Linux console. ## Location of your history By default, your history is stored in a file called `.bash_history` in your home directory. You can display the last five commands f.i. by running `tail -n5 ~/.bash_history`. > **On my own, since I'm using ZSH, my history is in `~/.zsh_history`.:** > > The easiest way to determine where your history is stored is to show the content of the `$HISTFILE` variable like this: `echo $HISTFILE`. > **Information:** > > Very recently, I had to take over a script developed by a colleague (let's call him JohnDoe) and I didn't know how to call it from the command line or what parameters to pass to it. > > As I'm a *root user* on the server, I simply consulted his history. All I had to do was display its `/home/john_doe/.bash_history` file. ### HISTIGNORE // HISTORY\_IGNORE variables The `HISTIGNORE` (for Bash) or the `HISTORY_IGNORE` (ZSH) variable can be used to specify some patterns you don't want to be saved in your history file. Let's take an example: `HISTIGNORE="sudo *"` will prevent all commands starting with `sudo` from being written to the history file. You can also use a pattern like `HISTIGNORE="*password*"` to prevent commands containing the word *password* from being written. To be able to specify more than one rule, just use the colon `:` character like in `HISTIGNORE="sudo *:*password*"`. ## Don't put a specific command in the history Imagine a command like `curl --user name:password http://www.example.com`. Did you really want to put that command (so your login and password in plain text) in the history file? Using [ZSH](/blog/tags/zsh), I just need to add a space before my command so by running `[SPACE]curl --user name:password http://www.example.com`. ```none # So, don't start your command like this: $ curl --user name:password http://www.example.com # v--- But just add a space character before $ curl --user name:password http://www.example.com ``` > **Information:** > > The space character is triggered by the `hist_ignore_space` option, defined in `.oh-my-zsh/lib/history.zsh`. ## Using grep to filter your history Let's say you wish to retrieve the latest `docker exec` instructions you've fired: `history | grep -i "docker exec"` i.e. simply *pipe* the output of the `history` command to the `grep` one and apply a filter (case insensitive using the `-i` flag). On my own, I don't need this because I'm using `CTRL`+`R` under ZSH. ## CTRL-R is your friend The `CTRL`+`R` keystrokes will display a small popup window (both in Bash or ZSH) and you'll be able to quickly search in your command's history. Below is the look and feel for ZSH; much better and easier than under Bash. ![CTRL-R in ZSH](./images/ctrl_r.webp) So press `CTRL`+`R` then start to type some letters like *gacom* and every entry having these letters (like **g**it add . ; git **com**mit ... will be retrieved). This popup is still a linear walk through a flat text file, though: no timestamp, no exit code, no idea which folder you were in. If that limitation starts to bite, [Atuin rebuilds the same `CTRL`+`R` shortcut on top of a SQLite database](/blog/atuin-bash-history) and keeps all that context for you. ## Autosuggestions in the console using ZSH By using ZSH and zsh-autosuggestion (see [my previous post](/blog/zsh-plugin-autosuggestions)) you don't even need to worry about the history anymore. Just start to type the first letters of a command like **doc** and all your previous **docker** fired statements will be accessible (just use navigation keys to select a previous command or type some more letters to be more restrictive). ## Delete an instruction from your history Ouch! You've forgotten to add a single space before your confidential instruction so, now, it's stored in the history. No panic! The history file is ... a file so just run `vi $HISTFILE` and you can edit it. Or, perhaps easier, just use `history -d` followed by the line number as displayed in the history output. So if the output of `history` is the one below, I can remove the `vi` line by running `history -d 2130` ```bash $ history -d 2130 2129 history | grep -i "docker exec" 2130 vi $HISTFILE 2131 history | grep -i "docker exec" ``` > **Information:** > > But, then the command `history -d 2130` will appear in the history, of course. So editing the file using a text editor is perhaps more ... discreet. > **Note:** > > You can specify a range like `history -d 2100-2130` (Bash 5.1+). --- # ZSH Functions - Customizing Your Shell for Docker Management > A collection of ZSH functions to enhance your terminal experience, including interactive Docker container management with fzf. Start new sessions, stop containers, and more with ease. --- ![ZSH Functions - Customizing Your Shell for Docker Management](/img/v2/zsh.webp) **TL;DR** Enhance your terminal workflow with custom ZSH functions for interactive Docker management. This article presents tools like `dex`, `dstop`, and `dnuke` that utilize `fzf` to let you quickly access shells, stop containers, view logs, and clean up resources without memorizing commands. On a daily basis, I use a few ZSH functions that I find useful. These functions can be added to your `~/.zshrc` file (or, better, to their own file — see [Beyond the Monolith - Organizing Your ZSH Workflow Like a Pro](/blog/modular-zsh-workflow)) to enhance your terminal experience while working with Docker. They provide quick and interactive ways to manage your Docker containers **without having to remember specific commands or container IDs** (and let's be honest, it's easy to forget them!). In this article, I'll share some of the ZSH functions I use for Docker management, including how to start a new terminal session in a running container, stop containers, access logs (even if the container is stopped), and clean up unused resources. These functions leverage `fzf` for an interactive selection process, making it easier to manage your Docker environment directly from your terminal. **Quick Jump:** [Start a new terminal session in a running Docker container](#start-a-new-terminal-session-in-a-running-docker-container) | [How to install](#how-to-install) Because these functions rely on `fzf`, if you don't have `fzf` installed, you can install it using your package manager (e.g., `sudo apt-get install fzf` on Debian-based systems). New to it? Check out [Introduction to fzf - Fuzzy Finder](/blog/linux-fzf-introduction) first. ## Start a new terminal session in a running Docker container Imagine you want to quickly access a running Docker container without having to remember its name or ID. The `dex` function allows you to do just that. It lists all running containers and lets you select one to start a new terminal session inside it. So, by simply running `dex` in your terminal, you can easily access any of your running Docker containers. This is especially useful when you have multiple containers running and want to quickly jump into one of them without having to look up its name or ID. ![Using dex to start a new terminal session in a running Docker container](./images/dex.webp) > **Editing the command before running it:** > > Press Enter to select the container and start the session, or press Ctrl+E to get the command in your prompt for editing — useful if you want to modify it before running it, for example to disable the entrypoint or mount a volume. > **Root access:** > > Press `Ctrl`+`R` while selecting a container with `dex` to start the session with root access. This is useful when you need to perform administrative tasks inside the container that require elevated privileges. > **Filtering:** > > Run `dex any_pattern` to filter the list of containers to the ones with that pattern. ## Stop one or more running Docker containers `dstop` is another useful function that allows you to stop one or more running Docker containers. Similar to `dex`, it uses `fzf` to provide an interactive interface for selecting the containers you want to stop. You can select multiple containers by using the multi-select feature of `fzf` i.e. pressing `Tab` to select multiple containers before pressing `Enter` to stop them. Press `CTRL`+`A` to select all containers at once. In the right pane, you can see information about the selected container(s) such as the container name, image, status, and ports. This can help you make informed decisions about which containers to stop. You can also see the live resource usage of the selected container(s) (CPU, memory, network I/O, block I/O) to identify which containers are consuming the most resources before deciding to stop them. ![Using dstop to stop one or more running Docker containers](./images/dstop.webp) ## Access logs of a running Docker container The `dlogs` function allows you to access the logs of a running Docker container. It also uses `fzf` to provide an interactive interface for selecting the container whose logs you want to view. In the right pane, you can see the logs of the selected container in real-time. This is particularly useful for debugging purposes, as you can monitor the logs as they are generated. You can use the scroll button of your mouse to scroll through the logs. ![dlogs function to access logs of a running Docker container](./images/dlogs.webp) ## Clean up unused Docker containers, images, volumes, and networks The dnuke function acts as an intelligent, interactive **Docker Cleanup Wizard** designed to [reclaim disk space](/blog/wsl-docker-vhdx-cleanup) without the risk of blindly deleting resources. Unlike a standard `docker system prune`, `dnuke` first scans your environment and presents a dynamic **Execution Plan**, showing you exactly what can be cleaned up (stopped containers, dangling images, unused volumes, and build cache). It processes cleanup in granular steps, allowing you to skip specific categories if needed. By default, it operates in a "safe" mode, targeting only dangling (untagged) images. `dnuke` supports CLI flags to customize its behavior: **dnuke flags** * **Deep Clean** — `dnuke -a` (or `--all`) removes **all** unused images, not just dangling ones (similar to `docker image prune -a`). * **Non-Interactive** — `dnuke -y` (or `--force`) bypasses the wizard and automatically confirms all steps. * **Help** — `dnuke --help` shows the full list of options and usage examples. If the environment is already clean, the wizard detects it immediately and exits without asking unnecessary questions. ![Getting dnuke help to see available options](./images/dnuke_help.webp) ![Running dnuke to clean up unused Docker resources](./images/dnuke.webp) ## Creating a new container The last function is `drun` to get the list of existing images and create a new container based on the selected one. > **Filtering:** > > Run `drun any_pattern` to filter the list of images to the ones with that pattern. ## One to rule them all The `dops` function is a wrapper around the other functions. It allows you to quickly access the other functions by using a single command. Simply run `dops` in your terminal, and it will list all the available functions. You can then select the function you want to use, and it will execute the corresponding command. Now, edit your `~/.zshrc` file and add the following line to source the `docker.zsh` file: `[[ -f ~/.zsh/docker.zsh ]] && source ~/.zsh/docker.zsh`. This will make the functions available in your terminal. ![Using dops to access the interactive menu for Docker management functions](./images/dops.webp) > **When you don't know which container yet:** > > `dex`, `dstop` and `dlogs` all assume you already know which container you want. For the moments you don't — five containers just started and you need to see CPU, memory and logs side by side to figure out which one is the problem — a dashboard beats a menu. That's the job of [lazydocker, run as a container](/blog/lazydocker). ## How to install Edit your `~/.zshrc` file and add the following lines at the end of the file: ```zsh fpath=(~/.zsh/docker-fns $fpath) autoload -Uz _d_check_env _d_print_cmd _d_has_running_containers autoload -Uz dex dlogs dnuke dops drun dstop # Setup alias for the interactive menu alias d='dops' # Print quick reference menu only in interactive sessions to avoid breaking scripts if [[ -o interactive ]]; then echo -e "\033[1;30mDocker Utils loaded:\033[0m" echo -e " \033[0;36mdex\033[0m (exec/root) \033[0;36mdlogs\033[0m (logs/json) \033[0;36mdstop\033[0m (stop/kill)" echo -e " \033[0;36mdnuke\033[0m (clean) \033[0;36mdops\033[0m (menu/alias \033[1;32md\033[0m) \033[0;36mdrun\033[0m (create new container)" fi ``` Now, create the `~/.zsh/docker-fns` directory and move the individual function files (`dex`, `dlogs`, `dnuke`, `dops`, `drun`, `dstop`) into that directory. This way, you can keep your functions organized and easily maintainable — the same `fpath`/`autoload` pattern is explained in more detail in [Beyond the Monolith - Organizing Your ZSH Workflow Like a Pro](/blog/modular-zsh-workflow). **Project: \~/.zsh/docker-fns** > **Note:** > > Now, edit your `~/.zshrc` file and add the lines mentioned above, in the blog post, to source the `docker.zsh` file and make the functions available in your terminal. This will allow you to use the `dex`, `dlogs`, `dnuke`, `dops`, `drun`, and `dstop` functions for managing your Docker containers directly from your terminal. **\_d\_check\_env** ``` #!/usr/bin/env zsh # Check for binary dependencies and Docker daemon status local dep for dep in "$@"; do if ! command -v "$dep" >/dev/null 2>&1; then echo >&2 -e "\033[0;31mError:\033[0m '$dep' is not installed." return 1 fi done # Fast daemon check to ensure Docker is responsive if ! docker info >/dev/null 2>&1; then echo >&2 -e "\033[0;31mError:\033[0m Docker daemon is not running or accessible." return 1 fi ``` **\_d\_print\_cmd** ``` #!/usr/bin/env zsh # Print executed commands with optional color formatting local cmd_str="$1" local color="${2:-\033[1;32m}" # Default to green local reset_color="\033[0m" # Truncate output if it exceeds 200 characters for readability if [[ ${#cmd_str} -gt 200 ]]; then cmd_str="${cmd_str:0:197}..." fi echo -e "Command: ${color}${cmd_str}${reset_color}" ``` **\_d\_has\_running\_containers** ``` #!/usr/bin/env zsh # Verify if at least one container is currently running # Utilizing 'read' is faster than piping to 'grep' if [[ -z "$(docker ps -q -n 1)" ]]; then echo >&2 -e "\033[0;33mInfo:\033[0m No running containers found." return 1 fi return 0 ``` **dex** ``` #!/usr/bin/env zsh # Interactive Docker Exec utilizing fzf _d_check_env fzf docker || return 1 _d_has_running_containers || return 0 local fzf_out key selection container_id cmd_exec user_flag query # Multi-line preview command using Docker Go templates # We use literal newlines here to ensure proper formatting inside fzf's subshell. # It extracts standard info, health state, ports, and the Docker Compose working directory. local preview_cmd='docker inspect --format "Container: {{.Name}} ---------------------------------------- Status: {{.State.Status}} Health: {{if .State.Health}}{{.State.Health.Status}}{{else}}No Healthcheck{{end}} Image: {{.Config.Image}} IP Address: {{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}} Ports: {{range \$p, \$conf := .NetworkSettings.Ports}}{{\$p}} {{end}} Compose Dir: {{index .Config.Labels \"com.docker.compose.project.working_dir\"}}" {1}' # Removed the --height parameter to allow a full-screen fzf experience fzf_out=$(docker ps --format "{{.ID}}\t{{.Names}}\t{{.Image}}" | \ fzf --layout=reverse --border --query="$query" \ --header "Enter: Run | Ctrl-E: Edit | Ctrl-R: Run as Root" \ --preview "$preview_cmd" --preview-window=right:50%:wrap \ --expect=ctrl-e,ctrl-r) # Exit if user canceled [[ -z "$fzf_out" ]] && return 0 key=$(head -n 1 <<< "$fzf_out") selection=$(tail -n +2 <<< "$fzf_out") container_id=$(awk '{print $1}' <<< "$selection") [[ -z "$container_id" ]] && return 0 # Toggle root user execution based on keypress user_flag="" if [[ "$key" == "ctrl-r" ]]; then user_flag="--user root" fi # Smart shell execution: attempts zsh, then bash, fallbacks to sh local smart_shell="sh -c 'exec \$(command -v zsh || command -v bash || echo sh)'" cmd_exec=$(echo "docker exec -it ${user_flag} ${container_id} ${smart_shell}" | tr -s ' ') # Place command in buffer for editing or execute immediately if [[ "$key" == "ctrl-e" ]]; then print -z "$cmd_exec" else _d_print_cmd "$cmd_exec" "\033[1;36m" eval "$cmd_exec" fi ``` **dlogs** ``` #!/usr/bin/env zsh # Interactive Docker Logs Viewer _d_check_env fzf docker || return 1 local fzf_out key selection container_id cmd_logs bat_cmd # Determine the best available syntax highlighter (batcat for Debian/Ubuntu) if command -v batcat >/dev/null 2>&1; then bat_cmd="batcat" elif command -v bat >/dev/null 2>&1; then bat_cmd="bat" fi # Define the base preview command for container metadata local preview_cmd='docker inspect --format "Container: {{.Name}} Status: {{.State.Status}} Compose Dir: {{index .Config.Labels \"com.docker.compose.project.working_dir\"}}" {1} echo "------------------------------------------------------------" ' # Append the log fetching command to the preview with color support if [[ -n "$bat_cmd" ]]; then # Use bat for rich, out-of-the-box log syntax highlighting preview_cmd+="docker logs --tail 50 {1} 2>&1 | $bat_cmd --language=log --color=always --style=plain" else # Fallback to awk to manually colorize ERROR/FATAL (red) and WARN/WARNING (yellow) local awk_colors='{gsub(/ERROR|FATAL|ERR/,"\033[1;31m&\033[0m"); gsub(/WARNING|WARN/,"\033[1;33m&\033[0m"); print}' preview_cmd+="docker logs --tail 50 {1} 2>&1 | awk '$awk_colors'" fi # Full-screen fzf experience with an expanded right panel. # CRITICAL: --ansi is required to render color codes inside the preview window. fzf_out=$(docker ps -a --format "{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Image}}" | \ fzf --ansi --layout=reverse --border \ --header "Enter: View Logs | Ctrl-E: Edit Command" \ --preview "$preview_cmd" --preview-window=right:60%:wrap \ --expect=ctrl-e) # Exit if user canceled [[ -z "$fzf_out" ]] && return 0 key=$(head -n 1 <<< "$fzf_out") selection=$(tail -n +2 <<< "$fzf_out") container_id=$(awk '{print $1}' <<< "$selection") [[ -z "$container_id" ]] && return 0 # Construct log command based on available tools (Priority: lnav > bat > standard) if command -v lnav >/dev/null 2>&1; then cmd_logs="docker logs -f --tail 500 ${container_id} 2>&1 | lnav" elif [[ -n "$bat_cmd" ]]; then cmd_logs="docker logs -f --tail 500 ${container_id} 2>&1 | ${bat_cmd} --language=log --style=plain --pager=\"less +F -R\"" else cmd_logs="docker logs -f --tail 500 ${container_id}" fi # Place command in buffer for editing or execute immediately if [[ "$key" == "ctrl-e" ]]; then print -z "$cmd_logs" else _d_print_cmd "$cmd_logs" "\033[1;36m" eval "$cmd_logs" fi ``` **dnuke** ``` #!/usr/bin/env zsh # Docker Cleanup Wizard _d_check_env docker || return 1 local force=0 local prune_all=0 # Function to display usage instructions local _show_help() { echo -e "\033[1;34mUsage: dnuke [OPTIONS]\033[0m" echo -e "Interactive Docker cleanup wizard to remove unused resources." echo -e "" echo -e "\033[1;33mOptions:\033[0m" echo -e " \033[1;32m-a, --all\033[0m Deep clean: Remove ALL unused images (not just dangling)." echo -e " \033[1;32m-y, --force\033[0m Non-interactive mode: Auto-confirm all steps." echo -e " \033[1;32m-h, --help\033[0m Show this help message." echo -e "" echo -e "\033[1;33mExamples:\033[0m" echo -e " dnuke # Standard cleanup (dangling images only)" echo -e " dnuke -a # Deep cleanup (all unused images)" echo -e " dnuke -y # Instant cleanup (no questions asked)" } # Parse arguments while [[ $# -gt 0 ]]; do case $1 in -y|--force) force=1; shift ;; -a|--all) prune_all=1; shift ;; -h|--help) _show_help; return 0 ;; *) echo -e "\033[0;31mUnknown option: $1\033[0m"; _show_help; return 1 ;; esac done echo -e "\033[1;34m=== Docker Cleanup Wizard ===\033[0m" echo -n "Scanning Docker environment... " # 1. Gather data silently local c_out v_out cache_size c_out=$(docker ps -a -f "status=exited" -q) v_out=$(docker volume ls -qf dangling=true) cache_size=$(docker system df | awk '/Build Cache/ {print $5}') # $5 usually holds the size # Safely populate containers and volumes arrays local -a c_ids v_ids i_ids [[ -n "$c_out" ]] && c_ids=("${(@f)c_out}") [[ -n "$v_out" ]] && v_ids=("${(@f)v_out}") local s_cont="pending" s_img="pending" s_vol="pending" s_cache="pending" [[ ${#c_ids[@]} -eq 0 ]] && s_cont="none" [[ ${#v_ids[@]} -eq 0 ]] && s_vol="none" [[ "$cache_size" == "0B" || "$cache_size" == "0" || -z "$cache_size" ]] && s_cache="none" local img_desc="" local img_cmd="" # Handle image logic: Dangling vs All Unused if [[ $prune_all -eq 1 ]]; then # Mode: --all # Logic: Get ALL image IDs, subtract IDs used by containers (running or stopped) local -a all_imgs=("${(@f)$(docker images -q)}") local -a used_imgs=() # Get images used by containers local c_all_imgs c_all_imgs=$(docker ps -a --format "{{.Image}}") if [[ -n "$c_all_imgs" ]]; then # We need to resolve image names to IDs to perform accurate subtraction # (This might be slow on huge systems, but accurate) used_imgs=("${(@f)$(docker inspect --format='{{.Id}}' ${(f)c_all_imgs} 2>/dev/null | sed 's/sha256://' | cut -c1-12)}") fi # Zsh Array Subtraction: i_ids = all_imgs - used_imgs i_ids=(${all_imgs:|used_imgs}) [[ ${#i_ids[@]} -eq 0 ]] && s_img="none" img_desc="Remove ${#i_ids[@]} unused image(s) (Deep Clean)" img_cmd="docker image prune -a -f" else # Mode: Standard (Dangling only) local i_out=$(docker images -f "dangling=true" -q) [[ -n "$i_out" ]] && i_ids=("${(@f)i_out}") [[ ${#i_ids[@]} -eq 0 ]] && s_img="none" img_desc="Remove ${#i_ids[@]} dangling image(s)" img_cmd="docker rmi ${i_ids[*]}" fi echo -e "Done.\n" # Helper function to display the dynamic roadmap (Execution Plan) _draw_plan() { local current_step=$1 echo -e "\033[1;36m[ Execution Plan ]\033[0m" local _fmt() { local step_name=$1 local state=$2 local desc=$3 local prefix=" " local color="\033[1;30m" # Dark Gray local step_status="[Pending]" if [[ "$step_name" == "$current_step" ]]; then prefix="➜ " color="\033[1;37m" # White (Current step) step_status="[Next] " elif [[ "$state" == "done" ]]; then color="\033[1;32m" # Green (Finished) step_status="[Done] " elif [[ "$state" == "skipped" ]]; then color="\033[0;33m" # Yellow (Skipped manually) step_status="[Skipped]" elif [[ "$state" == "none" ]]; then color="\033[1;30m" # Dark Gray (Nothing to do) step_status="[Empty] " fi echo -e "${prefix}${color}${step_status} ${desc}\033[0m" } _fmt "cont" "$s_cont" "Remove ${#c_ids[@]} stopped container(s)" _fmt "img" "$s_img" "$img_desc" _fmt "vol" "$s_vol" "Remove ${#v_ids[@]} unused volume(s)" _fmt "cache" "$s_cache" "Prune build cache (Size: ${cache_size:-0B})" echo "----------------------------------------" } # Fast-exit if the environment is already clean if [[ "$s_cont" == "none" && "$s_img" == "none" && "$s_vol" == "none" && "$s_cache" == "none" ]]; then _draw_plan "finish" echo -e "\033[1;32mEnvironment is already clean. Nothing to do!\033[0m" return 0 fi # Helper function to handle user prompts consistently local _ask() { if [[ $force -eq 1 ]]; then return 0; fi if read -q "?Execute this step? (y/N): "; then echo "\n" return 0 else echo -e "\n\033[0;33mSkipped.\033[0m\n" return 1 fi } # --- Step 1: Stopped Containers --- if [[ "$s_cont" == "pending" ]]; then _draw_plan "cont" docker ps -a -f "status=exited" --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" echo "" _d_print_cmd "docker rm ${c_ids[*]}" "\033[1;35m" if _ask; then docker rm "${c_ids[@]}" >/dev/null && echo -e "\033[1;32mContainers removed successfully.\033[0m\n" s_cont="done" else s_cont="skipped" fi fi # --- Step 2: Images (Dangling or All Unused) --- if [[ "$s_img" == "pending" ]]; then _draw_plan "img" if [[ $prune_all -eq 1 ]]; then # Warning specific to --all mode echo -e "\n\033[0;33mWarning: This will remove ALL images not currently used by a container.\033[0m" _d_print_cmd "$img_cmd" "\033[1;35m" if _ask; then eval "$img_cmd" >/dev/null && echo -e "\033[1;32mImages removed successfully.\033[0m\n" s_img="done" else s_img="skipped" fi else # Standard mode docker images -f "dangling=true" echo "" _d_print_cmd "$img_cmd" "\033[1;35m" if _ask; then docker rmi "${i_ids[@]}" >/dev/null && echo -e "\033[1;32mImages removed successfully.\033[0m\n" s_img="done" else s_img="skipped" fi fi fi # --- Step 3: Dangling Volumes --- if [[ "$s_vol" == "pending" ]]; then _draw_plan "vol" docker volume ls -f dangling=true echo "" _d_print_cmd "docker volume rm ${v_ids[*]}" "\033[1;35m" if _ask; then docker volume rm "${v_ids[@]}" >/dev/null && echo -e "\033[1;32mVolumes removed successfully.\033[0m\n" s_vol="done" else s_vol="skipped" fi fi # --- Step 4: Build Cache --- if [[ "$s_cache" == "pending" ]]; then _draw_plan "cache" local prune_cmd="docker builder prune -f" if docker buildx version >/dev/null 2>&1; then prune_cmd="docker buildx prune -f" fi _d_print_cmd "$prune_cmd" "\033[1;35m" if _ask; then eval "$prune_cmd" >/dev/null && echo -e "\033[1;32mCache pruned successfully.\033[0m\n" s_cache="done" else s_cache="skipped" fi fi # Final recap _draw_plan "finish" echo -e "\033[1;32mCleanup Wizard completed.\033[0m" ``` **dops** ``` #!/usr/bin/env zsh # Interactive Menu for Docker Operations _d_check_env fzf || return 1 local -a options=( "dex: Exec into a container (smart shell & root toggle)" "dlogs: View logs (smart viewer & JSON support)" "dnuke: Cleanup wizard (BuildKit aware)" "drun: Run a new ephemeral container from an image" "dstop: Batch stop or kill containers" ) local selected_op selected_op=$(print -l "${options[@]}" | \ fzf --height 30% --layout=reverse --header "Select Docker Operation" --border | \ awk -F':' '{print $1}') [[ -z "$selected_op" ]] && return 0 # Verify the selected function exists in the current Zsh session before executing if type "$selected_op" >/dev/null 2>&1; then "$selected_op" else echo >&2 "Error: Function '$selected_op' is not loaded or found." fi ``` **drun** ``` #!/usr/bin/env zsh # Interactive Docker Run (Ephemeral) _d_check_env fzf docker || return 1 local fzf_out key selection image_id cmd_run user_flag query query="$1" # Preview showing Image layers, Env vars and Labels local preview_cmd='docker inspect --format "Image: {{.RepoTags}} ---------------------------------------- ID: {{.Id}} Created: {{.Created}} Size: {{.Size}} Architecture: {{.Architecture}} ---------------------------------------- Labels: {{range \$k, \$v := .Config.Labels}}{{\$k}}={{\$v}} {{end}} " {2}' # Full-screen fzf to select image # Format: Repository:Tag | ID | Size | Created fzf_out=$(docker images --format "{{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}\t{{.CreatedAt}}" | \ fzf --layout=reverse --border --query="$query" \ --header "Enter: Run --rm | Ctrl-R: Run as Root" \ --preview "$preview_cmd" --preview-window=right:50%:wrap \ --expect=ctrl-r) # Exit if user canceled [[ -z "$fzf_out" ]] && return 0 key=$(head -n 1 <<< "$fzf_out") selection=$(tail -n +2 <<< "$fzf_out") image_id=$(awk '{print $2}' <<< "$selection") [[ -z "$image_id" ]] && return 0 # Toggle root user user_flag="" [[ "$key" == "ctrl-r" ]] && user_flag="--user root" # Smart shell execution local smart_shell="sh -c 'exec \$(command -v zsh || command -v bash || echo sh)'" cmd_run=$(echo "docker run --rm -it ${user_flag} ${image_id} ${smart_shell}" | tr -s ' ') _d_print_cmd "$cmd_run" "\033[1;36m" eval "$cmd_run" ``` **dstop** ``` #!/usr/bin/env zsh # Batch Stop/Kill Docker Containers _d_check_env fzf docker || return 1 _d_has_running_containers || return 0 local fzf_out key selection action_cmd color # Multi-line preview command rearranged for better UX: # 1. Core container metadata (IP, Ports, Compose). # 2. Live Resource Usage placed HIGH up so it's always visible (preventing accidental stops of busy containers). # 3. Mounts placed at the BOTTOM (since devcontainers can have massive lists). # Note: You can scroll the preview window in fzf using Shift-Up/Shift-Down or mouse wheel. local preview_cmd='docker inspect --format "Container: {{.Name}} Status: {{.State.Status}} Compose Dir: {{index .Config.Labels \"com.docker.compose.project.working_dir\"}} IP Address: {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} Ports: {{range \$p, \$conf := .NetworkSettings.Ports}}{{if \$conf}}{{\$p}} {{end}}{{end}}" {1} echo "------------------------------------------------------------" echo -e "\033[1;33m>>> Live Resource Usage (Snapshot):\033[0m" docker stats --no-stream --format "CPU: {{.CPUPerc}} | MEM: {{.MemUsage}} | NET: {{.NetIO}} | BLOCK I/O: {{.BlockIO}}" {1} echo "------------------------------------------------------------" echo -e "\033[1;34m>>> Mounts (Scroll down to see all):\033[0m" docker inspect --format "{{range .Mounts}}• {{.Source}} -> {{.Destination}} {{end}}" {1}' # Full-screen fzf with multi-selection enabled (-m) fzf_out=$(docker ps --format "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}" | \ fzf -m --ansi --layout=reverse --border \ --header "TAB: Select | Ctrl-A: Select All | Enter: Stop | Ctrl-K: Kill (Scroll preview with Shift+Up/Down)" \ --bind "ctrl-a:select-all" \ --expect=ctrl-k \ --preview "$preview_cmd" --preview-window=right:55%:wrap) [[ -z "$fzf_out" ]] && return 0 # Extract the key pressed (Enter or ctrl-k) key=$(head -n 1 <<< "$fzf_out") # Extract only the container IDs (first column) from the selected rows selection=$(tail -n +2 <<< "$fzf_out" | awk '{print $1}') [[ -z "$selection" ]] && return 0 # Convert selection string into a proper Zsh array local -a target_ids=("${(@f)selection}") # Default action is graceful stop action_cmd="docker stop" color="\033[1;33m" # Yellow color for stop # Switch to forceful kill if triggered by Ctrl-K if [[ "$key" == "ctrl-k" ]]; then action_cmd="docker kill" color="\033[1;31m" # Red color for kill fi # Print and execute the command _d_print_cmd "${action_cmd} ${target_ids[*]}" "$color" eval "${action_cmd} ${target_ids[@]}" ``` --- # Beyond the Monolith - Organizing Your ZSH Workflow Like a Pro > Stop bloating your shell configuration. Learn why moving ZSH functions to standalone files improves performance and maintainability, featuring a universal project navigator. --- ![Modular ZSH Workflow](/img/v2/repo_with_fzf.webp) **TL;DR** A bloated `~/.zshrc` slows down your terminal and makes debugging a nightmare. By using ZSH's `fpath` and modularizing your functions into `~/.zsh/fns`, you gain instant shell startup and cleaner code. This article explains the "why" and provides a "Super Function" to navigate your projects instantly. We've all been there: your `~/.zshrc` starts as a 10-line file and ends up as a 1,500-line monster. It contains everything from theme settings to complex Docker scripts and random aliases you forgot you wrote. This is the **Monolithic Shell Anti-pattern** (if you're starting from scratch, see [my ZSH installation guide](/blog/zsh-install) first). In this article, I'll show you why you should move your logic into standalone files within `~/.zsh/fns` and how ZSH's `autoload` mechanism can make your terminal feel snappier than ever. To top it off, I'll share `repo`, a function that will change how you navigate your workspace. **Quick Jump:** [Teleporting to a Project](#teleporting-to-a-project) | [How to Set It Up](#how-to-set-it-up) ## Teleporting to a Project Once `repo` is set up (covered below), typing a fragment of a project name jumps straight into it — no `cd`, no typing the full path: ```bash $ repo blg Switched to: ~/repositories/blog $ pwd ~/repositories/blog ``` Only one project matched `blg` among everything under `~/repositories`, so `repo` skipped the picker entirely and `cd`'d straight in. If more than one project had matched, the same command would have opened an `fzf` picker pre-filtered with those results instead. ## The Problem with the Monolith When you put a function directly into your `.zshrc`, ZSH has to parse and load that code into memory **every single time** you open a new tab. If you have dozens of helper functions for [Docker](/blog/zsh-docker-functions), Kubernetes, or [Git](/blog/git-branches-gst), those milliseconds add up. By moving functions to a dedicated folder (like `~/.zsh/fns`), you unlock three major benefits: 1. **Lazy Loading:** Using `autoload`, ZSH only reads the file when you actually type the command. Your shell starts instantly. 2. **Maintainability:** If one of your custom functions breaks, you go to the `fns` folder and fix it. You don't have to scroll through a sea of code. 3. **Portability:** You can version-control your functions folder separately and share it across different machines without carrying over your entire OS-specific config. ## The Secret Sauce: `fpath` and `autoload` Instead of `source`-ing files, we use the `fpath`. This is an array of directories where ZSH looks for function definitions. When you call a function, ZSH looks into these folders. If it finds a file matching the command name, it loads it on the fly. ## 💎 The Super Function: `repo` (Universal Project Navigator) As developers, we switch contexts constantly. We jump between a PHP backend, an API project, a Vue.js frontend, a Python application ... and between multiple documentation folders — all day long. Doing `cd ~/repositories/project-xyz` hundreds of times a day is a waste of life. The `repo` function uses `fd` (or `find`) and `fzf` to let you search all your git repositories and: 1. **Jump** to the folder. 2. **Open** your favorite editor (VS Code, Cursor, Neovim). ### Why this is better in a standalone file This function contains logic to handle directory depth and editor launching. In your `.zshrc`, it's clutter. In `~/.zsh/fns/repo`, it's a dedicated tool. > **Installation Requirement:** > > This function requires `fzf` (see [Introduction to fzf](/blog/linux-fzf-introduction) if you're not familiar with it yet) and `fd` (or `find`). For the best experience, install `fd-find`. ## How to Set It Up ### 1. Create the structure First, create the folder where your "autonomous" functions will live: ```bash mkdir -p ~/.zsh/fns ``` ### 2. Configure your `~/.zshrc` Add these lines to your `.zshrc`. This is the only "boilerplate" you'll ever need. It tells ZSH: "Look in this folder, and if you find files there, consider them functions I might want to use." ```zsh # Anonymous utilities autoloading fpath=(~/.zsh/fns $fpath) # -U Suppress alias expansion for standard behavior # -z Load using zsh style # ::t Don't load the function yet, just index it; will be loaded at the first use. autoload -Uz ~/.zsh/fns/*(.:t) ``` ### 3. Create the `repo` function file Create a file at `~/.zsh/fns/repo` (no extension!) and paste the following code. **Project: \~/.zsh/fns** > **Note:** > > Ensure the filename is exactly "repo". Do not add .sh or .zsh. **repo** ``` #!/bin/zsh # Description: Navigate to projects and optionally open in an editor. # Usage: repo [search_path] # Private helper for repo to check dependencies. _repo_check_deps() { local missing_dep=0 local os_type os_type=$(uname -s) # Check for fzf if ! command -v fzf >/dev/null 2>&1; then printf "\033[0;31m[Error]\033[0m 'fzf' is not installed.\n" missing_dep=1 fi # Check for fd or fdfind if ! command -v fd >/dev/null 2>&1 && ! command -v fdfind >/dev/null 2>&1; then printf "\033[0;31m[Error]\033[0m 'fd' is not installed.\n" missing_dep=1 fi if [[ "$missing_dep" -eq 1 ]]; then printf "\033[0;33mHow to install:\033[0m\n" if [[ "$os_type" == "Darwin" ]]; then printf " brew install fzf fd\n" elif [[ -f /etc/debian_version ]]; then printf " sudo apt update && sudo apt install fzf fd-find\n" elif [[ -f /etc/fedora-release ]]; then printf " sudo dnf install fzf fd-find\n" else printf " Please install 'fzf' and 'fd' using your package manager.\n" fi return 1 fi return 0 } # 1. Run the private dependency check # Since _repo_check_deps is also in fpath, we can call it directly. if ! _repo_check_deps; then return 1 fi # 2. Determine the correct fd binary local fd_bin if command -v fd >/dev/null 2>&1; then fd_bin="fd" else fd_bin="fdfind" fi # 3. Setup Editor: Use $EDITOR, or fallback to 'code' local visual_editor="${EDITOR:-code}" # 4. Search logic local search_path="$HOME/repositories" local query="$1" local selected_dir selected_dir=$("$fd_bin" '^\.git$' "$search_path" \ --hidden --no-ignore --type d --max-depth 3 | \ sed 's|/\.git/\?$||' | \ fzf --reverse --height 40% --border --header 'Navigate to Project' \ --query "$query" --select-1 --exit-0) # 5. Handling exit if [[ -z "$selected_dir" ]]; then return 0 fi # Clean up trailing /.git selected_dir="${selected_dir%/.git}" selected_dir="${selected_dir%/.git/}" cd "$selected_dir" || return 1 printf "Switched to: %s\n" "$selected_dir" # 6. Open in Editor # We check if the editor command actually exists on the machine if command -v "$visual_editor" >/dev/null 2>&1; then # [Y/n] is the standard way to show Y is the default printf "Open in %s? [Y/n] " "$visual_editor" read -k 1 REPLY printf "\n" # Check if REPLY is a newline (Enter) OR Y/y if [[ "$REPLY" == $'\n' || "$REPLY" =~ ^[Yy]$ ]]; then "$visual_editor" . fi else printf "\033[0;33mNote:\033[0m Editor '%s' not found. Set \$EDITOR in your .zshrc\n" "$visual_editor" fi ``` > **Note:** > > Remember to update the `search_path` variable to where you're saving your projects and, if you aren't using VSCode, also update the local `visual_editor` variable. ### 4. Use it Create a new terminal or run `source ~/.zshrc` (because you've updated `.zshrc`) and simply run `repo` to start the function. How cool is that? > **Reload:** > > If you've already used the alias and, for some reason, you've updated the code, the command to run is: `unfunction repo && autoload -Uz repo && repo`. ## More Demos ### Fast Search (Fuzzy Find) You can speed up your navigation by passing a search pattern directly to the command: `repo ` — exactly the `repo blg` example demonstrated at the top of this article. * If multiple projects match your query, `fzf` will open pre-filtered with those results. * If **only one** project matches, the script will skip the interface entirely and `cd` into the directory immediately. > **Pro Tip:** > > This is perfect for projects you visit often. If your pattern is unique enough, it becomes a lightning-fast shortcut! ## Conclusion The difference between a "junior" and "senior" developer environment often lies in **tooling friction**. By moving your functions into autonomous files, you reduce the friction of maintaining your environment. With `repo` living in your new modular setup, you can now teleport across your workspace in seconds. Next time you write a useful snippet, don't paste it in your `.zshrc`. Give it its own home in `~/.zsh/fns/` — see [ZSH Functions - Customizing Your Shell for Docker Management](/blog/zsh-docker-functions) and [Showing the last 3 updated branches when you jump in a git repo](/blog/git-branches-gst) for other sets of functions and hooks organized this same way. ---