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.
#!/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.
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:
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.
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
- ohmyzsh/ohmyzsh, repository and the 372-plugin count, MIT license, read on 18 October 2026.
- Installing ZSH, project wiki, with the per-distro commands, updated 28 July 2025.
- Plugin list for oh-my-zsh, where
git,zandextracteach carry their own README.
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.