>_ josetilabs.com
← blog

18 Oct 2026 · linux, terminal, produtividade · 5 min

How to make a Linux terminal that looks good and works

A hundred-line script took my prompt from 83 ms to 195 ms and gave me three commands I did not have. What held me back was not the looks. It was a compinit I wrote one time too many, costing 2.2 seconds on every single terminal open.

A pretty shell is cheap. A pretty shell that adds two seconds to every open is what most tutorials install without measuring.

The script

The terminal-zsh.sh below installs zsh through your distribution’s package manager, clones oh-my-zsh, enables three plugins and writes a fresh .zshrc. Your old file becomes .zshrc.bak-AAAAMMDD-HHMMSS before anything is written, and the script exits 0 even on a machine with no network, where it writes what it can and tells you what it skipped.

bash
#!/usr/bin/env bash
# terminal-zsh.sh - instala zsh, oh-my-zsh e tres plugins, e prova que o shell abre.
#
# Uso:
#   ./terminal-zsh.sh              instala e configura para o usuario atual
#   ./terminal-zsh.sh --verificar  nao instala nada, so confere o que existe
#   ./terminal-zsh.sh --medir      mede quanto tempo o shell leva para abrir
set -u

DATA=$(date +%Y%m%d-%H%M%S)
ZSH_DIR="${ZSH:-$HOME/.oh-my-zsh}"
RC="$HOME/.zshrc"
BAK="$RC.bak-$DATA"
PLUGINS="git z extract"
MODO="instalar"

case "${1:-}" in
    --verificar) MODO="verificar" ;;
    --medir)     MODO="medir" ;;
    "")          MODO="instalar" ;;
    *) printf 'uso: %s [--verificar|--medir]\n' "$0"; exit 2 ;;
esac

log() { printf '%s\n' "$*"; }
tem_cmd() { command -v "$1" >/dev/null 2>&1; }

# Rede sem depender de ping: abre uma conexao e fecha. Sem resposta, 4s e volta.
tem_rede() { timeout 4 bash -c 'exec 3<>/dev/tcp/github.com/443' >/dev/null 2>&1; }

gerenciador() {
    local g
    for g in apt-get dnf pacman apk; do
        if tem_cmd "$g"; then printf '%s' "$g"; return 0; fi
    done
    return 1
}

instalar_pacotes() {
    local g="$1" sudo_cmd=""
    if [ "$(id -u)" -ne 0 ]; then
        if ! tem_cmd sudo; then return 1; fi
        sudo_cmd="sudo"
    fi
    # ca-certificates entra na lista porque sem o pacote store o git clone do
    # github falha com "unable to access" em vez de "sem rede", e o script
    # acaba achando que a rede caiu.
    if [ "$g" = "apt-get" ]; then
        $sudo_cmd apt-get update -qq >/dev/null 2>&1
        $sudo_cmd apt-get install -y --no-install-recommends zsh git ca-certificates >/dev/null 2>&1
    elif [ "$g" = "dnf" ]; then
        $sudo_cmd dnf install -y zsh git ca-certificates >/dev/null 2>&1
    elif [ "$g" = "pacman" ]; then
        $sudo_cmd pacman -S --noconfirm --needed zsh git ca-certificates >/dev/null 2>&1
    elif [ "$g" = "apk" ]; then
        $sudo_cmd apk add --no-cache zsh git ca-certificates >/dev/null 2>&1
    else
        return 1
    fi
}

instalar_omz() {
    if [ -f "$ZSH_DIR/oh-my-zsh.sh" ]; then
        log "oh-my-zsh: ja instalado em $ZSH_DIR"
        return 0
    fi
    if ! tem_cmd git; then
        log "oh-my-zsh: git ausente, pulei (o framework exige git)"
        return 1
    fi
    if ! tem_rede; then
        log "oh-my-zsh: sem rede para o github, pulei"
        return 1
    fi
    git clone --depth=1 https://github.com/ohmyzsh/ohmyzsh.git "$ZSH_DIR" >/dev/null 2>&1
    if [ -f "$ZSH_DIR/oh-my-zsh.sh" ]; then
        log "oh-my-zsh: instalado em $ZSH_DIR"
        return 0
    fi
    log "oh-my-zsh: o clone falhou, segui sem o framework"
    return 1
}

# Configuracao com o framework: os tres plugins, prompt com branch e historico
# grande sem repeticao. O bloco do framework fica no topo porque plugins e tema
# sao lidos na hora do source.
config_com_omz() {
    local p
    {
        printf 'export ZSH="%s"\n' "$ZSH_DIR"
        printf 'ZSH_THEME="robbyrussell"\n'
        printf 'plugins=(\n'
        for p in $PLUGINS; do printf '  %s\n' "$p"; done
        printf ')\n'
        printf 'source $ZSH/oh-my-zsh.sh\n'
        cat <<'ZSHRC'

# historico: 50 mil linhas, sem comando repetido em cima do anterior
HISTSIZE=50000
SAVEHIST=50000
HIST_IGNORE_ALL_DUPS=1
HIST_IGNORE_SPACE=1

# completacao: o framework ja chama compinit, entao reaproveitamos o dump dele
autoload -Uz compinit && compinit -d "$ZSH_COMPDUMP"

# git: gs, ga, gc (commit), gca (commit com amend) e gl (log curto)
alias gcam='git commit -am'
# z: pula para diretorio pelo nome, com frequencia
# extract: abre .tar.gz, .zip, .7z e companhia com um comando
ZSHRC
    } > "$RC.tmp"
}

# Configuracao sem o framework: mesmos atalhos, na dependencia. Serve para o
# .zshrc nao ficar apontando para um source que nao existe.
config_sem_omz() {
    {
        printf '# zsh sem oh-my-zsh: atalhos minimos, sem source quebrado\n'
        printf 'HISTSIZE=50000\nSAVEHIST=50000\nHIST_IGNORE_ALL_DUPS=1\n'
        printf 'autoload -Uz compinit && compinit -d "$HOME/.zcompdump"\n'
        printf 'alias gcam=%s\n' "'git commit -am'"
        printf 'alias gs=%s\n' "'git status -sb'"
        printf 'PROMPT=%s\n' "'%F{cyan}%~%f %# '"
    } > "$RC.tmp"
}

escrever_rc() {
    if [ -f "$RC" ]; then
        cp -p "$RC" "$BAK"
        log "backup: $RC -> $BAK"
    fi
    if [ -f "$ZSH_DIR/oh-my-zsh.sh" ]; then
        config_com_omz
    else
        config_sem_omz
    fi
    mv "$RC.tmp" "$RC"
    log "escrito: $RC"
}

conferir_plugins() {
    local pachados=0 p
    for p in $PLUGINS; do
        if [ -d "$ZSH_DIR/plugins/$p" ]; then
            pachados=$((pachados + 1))
        else
            log "plugin ausente: $p"
        fi
    done
    printf 'plugins: %s de %s presentes\n' "$pachados" "$(printf '%s\n' $PLUGINS | wc -l | tr -d ' ')"
}

provar_shell() {
    if ! tem_cmd zsh; then
        log "zsh: nao instalado, a prova de abertura ficou para depois"
        return 1
    fi
    zsh --version
    if ! zsh -n "$RC" 2>/dev/null; then
        log "erro de sintaxe no .zshrc: o shell nao abre com esse arquivo"
        return 1
    fi
    log "sintaxe do .zshrc: ok"
    if zsh -i -c 'printf "shell abriu com %s\n" "$ZSH_VERSION"' 2>/dev/null; then
        log "shell interativo: respondeu"
    else
        log "shell interativo: falhou ao iniciar (rode zsh -i e veja o erro)"
    fi
}

# Media de cinco aberturas, em milissegundos. O primeiro arranque fica de fora
# porque ele paga o cache frio e distorce a media.
medir_partida() {
    local i t0 t1 soma=0 total=5
    if ! tem_cmd zsh; then
        log "zsh ausente: sem o que medir"
        return 1
    fi
    zsh -i -c 'exit 0' >/dev/null 2>&1
    for i in 1 2 3 4 5; do
        t0=$(date +%s%N)
        zsh -i -c 'exit 0' >/dev/null 2>&1
        t1=$(date +%s%N)
        soma=$(( soma + (t1 - t0) / 1000000 ))
    done
    printf 'abertura do shell: media de %s execucoes, %s ms\n' "$total" "$(( soma / total ))"
}

if [ "$MODO" = "instalar" ]; then
    if ! tem_cmd zsh; then
        if g=$(gerenciador); then
            log "zsh ausente: instalando com $g"
            if instalar_pacotes "$g" && tem_cmd zsh; then
                log "zsh instalado: $(zsh --version)"
            else
                log "zsh nao instalado (sem root ou sem rede): o resto do script continua"
            fi
        else
            log "zsh ausente e sem gerenciador de pacotes conhecido"
        fi
    else
        log "zsh: $(zsh --version)"
    fi
    instalar_omz || true
    escrever_rc
fi

if [ "$MODO" != "instalar" ]; then
    log "modo $MODO: nada foi instalado nem reescrito"
    [ -f "$RC" ] && log ".zshrc encontrado" || log ".zshrc ausente"
    [ -f "$ZSH_DIR/oh-my-zsh.sh" ] && log "oh-my-zsh encontrado" || log "oh-my-zsh ausente"
fi

[ -f "$ZSH_DIR/oh-my-zsh.sh" ] && conferir_plugins
provar_shell || true
[ "$MODO" = "medir" ] && medir_partida || true

log "fim: abra um terminal novo e rode zsh"
exit 0

None of the three plugins is magic. git brings the short aliases (gst for status, gca for commit with amend), z turns cd /long/path into z reports, and extract opens .tar.gz, .zip and .7z with one command instead of four. That is the difference between typing and running.

Two terminal panels: before, bash showing four repeated git status lines in the history and a full directory path; after, zsh with the git branch in the prompt, the gst alias, the z command jumping to a directory and extract opening an archive
What changes in practice: history without repeats, the branch in the prompt, and three commands that replace what I typed every day.

How to verify

Run ./terminal-zsh.sh, then open a new terminal. Here is the output of a full run in an ubuntu 24.04 container:

text
zsh ausente: instalando com apt-get
zsh instalado: zsh 5.9 (x86_64-ubuntu-linux-gnu)
oh-my-zsh: instalado em /root/.oh-my-zsh
escrito: /root/.zshrc
plugins: 3 de 3 presentes
zsh 5.9 (x86_64-ubuntu-linux-gnu)
sintaxe do .zshrc: ok
shell abriu com 5.9
shell interativo: respondeu
fim: abra um terminal novo e rode zsh

plugins: 3 de 3 presentes and shell interativo: respondeu are the two lines that matter. The first says the three plugins really exist; the second says a real interactive shell comes up with that file, which is a different claim from the file having valid syntax.

The compinit I wrote one time too many

Oh-my-zsh already calls compinit on its own, on line 238 of oh-my-zsh.sh, pointing at the $ZSH_COMPDUMP file. When I added a second compinit with the path every tutorial shows, compinit -d ~/.zcompdump, the shell went from 195 ms to 2414 ms.

Bar chart of shell startup time in milliseconds: plain zsh 83 ms, zsh with oh-my-zsh and three plugins 195 ms, the same script with an extra compinit writing to a separate dump 2414 ms, and pointing compinit at the framework dump 214 ms
The fourth bar is 2.2 seconds taller than the second. Same script, one word changed: the dump path.

The reason is that the second dump had nothing to be reused from. I checked the file between two opens: its 49699 bytes were rewritten, while the framework’s own dump stayed identical. A compinit whose file is rebuilt on every open does the expensive work every time. Pointed at $ZSH_COMPDUMP, the same command costs 214 ms.

The script’s .zshrc uses compinit -d "$ZSH_COMPDUMP", which is why it never walks into that trap.

If it goes wrong

The clone fails and the script blames the network. With --no-install-recommends, ca-certificates is left out and git fails with unable to access instead of a DNS error. That is why ca-certificates sits in the script’s package list.

Startup got slow after you edited something. Run ./terminal-zsh.sh --medir and look at the number. Above 300 ms it is almost always a duplicated compinit, or one pointing at a new path.

Your .zshrc now sources a file that does not exist. That happens when the clone failed after your old file was moved aside. The backup is at ~/.zshrc.bak-AAAAMMDD-HHMMSS; copy it back to undo. The script also has a no-framework path that writes a prompt and aliases without the source.

oh-my-zsh against the alternatives

option install what you get measured cost
plain zsh package manager prompt, history, completion 83 ms
zsh + oh-my-zsh git clone 372 plugins, 140 themes, autocomplete 195 ms
starship single binary via cargo fast prompt, no framework not measured here
bash + bash-it git clone bash plugins not measured here

Starship is the serious competitor: one binary, none of the 16 MB clone. Oh-my-zsh wins on ready-made plugins and loses on startup time and disk. If what you want is a good-looking prompt, starship does it in one line.

The rest of the machine benefits from the script too. The same rule applies: let the pieces maintain themselves instead of relying on someone remembering to run chsh.

FAQ

Do I need oh-my-zsh for those three plugins? No. git and extract are an alias and a handful of lines, and z can be the 20-line script from the project itself. The framework is convenience, not a dependency.

Does the script work on Arch and Fedora? Yes, it detects apt-get, dnf, pacman and apk and uses each one’s install path. I tested the apt one, not all four.

Does it switch my default shell? No, on purpose. The script does not touch /etc/passwd. Run chsh -s "$(command -v zsh)" when you want it, and check with echo $SHELL in a new session.

Is robbyrussell the best theme? It is the safest: plain ASCII, no Powerline font required. Themes with arrows and icons need a font installed first, and that is where most people think they broke zsh.

Sources

Measurements in a disposable ubuntu 24.04 container capped at 0.5 CPU and 256 MB of RAM: zsh 5.9, oh-my-zsh cloned with --depth=1, median of seven shell opens after one warm-up run. Method details and all seven readings per point are in the SSH key post, measured the same way.

Read next

// By e-mail

Posts published here also go out by e-mail. Delivery has not started yet: whoever leaves an address now joins the first batch.

I store only your e-mail, the date and the record of this consent. No cookies, no trackers, nothing shared with third parties. Details in the privacy policy.