Truques para criar aliases e funções bash persistentes por projeto

1. Fundamentos da Persistência por Projeto

1.1. Diferença entre aliases globais (~/.bashrc) e locais por projeto

Aliases globais definidos em ~/.bashrc ou ~/.bash_aliases são carregados uma vez no início da sessão e permanecem disponíveis em qualquer diretório. Isso funciona bem para comandos universais como ll='ls -la', mas se torna problemático quando você trabalha em múltiplos projetos com contextos diferentes. Por exemplo, um alias run='npm start' pode ser adequado para um projeto Node.js, mas não para um projeto Python que usa run='python app.py'.

Aliases por projeto resolvem esse conflito carregando definições específicas apenas quando você está no diretório do projeto correspondente.

1.2. Por que scripts .env ou .bash_project são superiores a variáveis temporárias

Variáveis temporárias definidas manualmente com export se perdem ao fechar o terminal. Scripts como .env ou .bash_project oferecem persistência controlada: são arquivos versionáveis, podem conter lógica condicional e são facilmente compartilháveis entre membros da equipe via repositório.

1.3. Estrutura de diretório recomendada

Duas abordagens principais:

  • Centralizada: ~/.project-aliases/projeto-x.sh — mantém todos os aliases em um local, mas requer manutenção manual de referências.
  • Distribuída: .bash_aliases dentro do projeto — cada projeto carrega seu próprio arquivo, facilitando o versionamento.

A abordagem distribuída é mais recomendada por ser autocontida e portátil.

2. Técnica do Arquivo .env com Fonte Automática

2.1. Criando um arquivo .env com aliases específicos

Crie um arquivo .env na raiz do projeto:

# .env do projeto
alias build='docker build -t meu-projeto .'
alias test='pytest tests/'
alias run='python main.py'
function deploy() {
  echo "Fazendo deploy da branch $(git branch --show-current)..."
  git push origin main
}

2.2. Usando PROMPT_COMMAND para carregar automaticamente

Adicione ao seu ~/.bashrc:

__load_project_env() {
  local env_file=".env"
  if [ -f "$env_file" ] && [ "$PWD" != "$__LAST_PWD" ]; then
    source "$env_file"
    __LAST_PWD="$PWD"
    echo "[PROJETO] Aliases carregados de $PWD/$env_file"
  fi
}
PROMPT_COMMAND="__load_project_env; $PROMPT_COMMAND"

2.3. Script auto_source.sh que percorre diretórios pai

Para projetos aninhados, um script que busca arquivos nos diretórios pai:

# auto_source.sh
__find_and_source() {
  local dir="$PWD"
  while [ "$dir" != "/" ]; do
    if [ -f "$dir/.env" ]; then
      source "$dir/.env"
      return
    fi
    dir=$(dirname "$dir")
  done
}
PROMPT_COMMAND="__find_and_source; $PROMPT_COMMAND"

3. Funções Bash com Detecção de Diretório

3.1. Função project_alias que verifica $PWD

project_alias() {
  local cmd="$1"
  case "$PWD" in
    */projeto-x/*)
      case "$cmd" in
        build) docker-compose build ;;
        test) npm test ;;
        *) echo "Comando desconhecido para este projeto" ;;
      esac
      ;;
    */projeto-y/*)
      case "$cmd" in
        build) mvn clean install ;;
        test) mvn test ;;
        *) echo "Comando desconhecido para este projeto" ;;
      esac
      ;;
    *)
      echo "Nenhum projeto reconhecido neste diretório"
      ;;
  esac
}

3.2. Uso de case ou if para mapear pastas

function deploy() {
  if [[ "$PWD" == *"/projeto-x"* ]]; then
    echo "Deploy do projeto X..."
    docker push meu-projeto:latest
  elif [[ "$PWD" == *"/projeto-y"* ]]; then
    echo "Deploy do projeto Y..."
    git push heroku main
  else
    echo "Projeto não reconhecido para deploy"
  fi
}

3.3. Exemplo prático: deploy() condicional

function docker_up() {
  if [ -f "docker-compose.yml" ]; then
    docker-compose up -d
  else
    echo "docker-compose.yml não encontrado em $PWD"
    return 1
  fi
}

4. Gerenciamento com Git Hooks e .git/info/

4.1. Hook post-checkout para recarregar aliases

Crie .git/hooks/post-checkout:

#!/bin/bash
# Recarrega aliases após trocar de branch
if [ -f ".bash_aliases" ]; then
  source ".bash_aliases"
  echo "[GIT] Aliases recarregados do branch $(git branch --show-current)"
fi

4.2. Hook post-merge para atualizar funções

#!/bin/bash
# Atualiza funções após merge que modifica .bash_project
if git diff HEAD@{1} --name-only | grep -q ".bash_project"; then
  source ".bash_project"
  echo "[GIT] Funções atualizadas após merge"
fi

4.3. Armazenando aliases em .git/info/aliases

Para não poluir o repositório, use o diretório .git/info/ (não versionado):

# .git/info/aliases
alias gs='git status'
alias gc='git commit'
alias gp='git push'

E no ~/.bashrc:

if [ -f ".git/info/aliases" ]; then
  source ".git/info/aliases"
fi

5. Integração com direnv e Ferramentas Similares

5.1. Configuração básica do direnv

Instale o direnv e adicione ao ~/.bashrc:

eval "$(direnv hook bash)"

5.2. Exemplo de .envrc com aliases

# .envrc
layout python
alias run='flask run'
alias test='pytest -v'
function lint() {
  flake8 src/ tests/
}

O direnv carrega automaticamente ao entrar no diretório e descarrega ao sair.

5.3. Alternativa leve: função cd personalizada

cd() {
  builtin cd "$@" && [ -f ".project_functions" ] && source ".project_functions"
}

6. Funções com Namespace para Evitar Conflitos

6.1. Prefixo de projeto

# Em vez de 'build', use 'proj_x_build'
function proj_x_build() { docker build -t proj-x .; }
function proj_x_test() { pytest tests/; }

6.2. Função wrapper com verificação de conflito

safe_alias() {
  local name="$1"
  local cmd="$2"
  if type "$name" &>/dev/null; then
    echo "AVISO: Alias '$name' já existe globalmente. Usando '${name}_local'"
    alias "${name}_local=$cmd"
  else
    alias "$name=$cmd"
  fi
}

6.3. Uso de declare -f e type para evitar sobrescrita

if ! declare -f build &>/dev/null; then
  function build() { echo "Build padrão"; }
fi

7. Automação de Carga com trap e PROMPT_COMMAND

7.1. Script que adiciona verificação ao PROMPT_COMMAND

__check_project_aliases() {
  local current_dir="$PWD"
  if [ "$current_dir" != "$__LAST_PROJECT_DIR" ]; then
    __unload_project_aliases
    if [ -f "$current_dir/.bash_aliases" ]; then
      source "$current_dir/.bash_aliases"
      __LAST_PROJECT_DIR="$current_dir"
      __LAST_ALIASES_HASH=$(md5sum "$current_dir/.bash_aliases" 2>/dev/null)
    fi
  fi
}
PROMPT_COMMAND="__check_project_aliases; $PROMPT_COMMAND"

7.2. Função __load_project_aliases executada a cada prompt

__load_project_aliases() {
  local alias_file="$PWD/.bash_aliases"
  local current_hash=$(md5sum "$alias_file" 2>/dev/null | cut -d' ' -f1)
  if [ -f "$alias_file" ] && [ "$current_hash" != "$__ALIAS_HASH" ]; then
    source "$alias_file"
    __ALIAS_HASH="$current_hash"
    echo "[ALIAS] Carregado: $alias_file"
  fi
}

7.3. Cache de md5sum para evitar recarregar desnecessariamente

__ALIAS_CACHE=""
__load_with_cache() {
  local file="$PWD/.bash_aliases"
  local new_hash=$(md5sum "$file" 2>/dev/null)
  if [ -f "$file" ] && [ "$new_hash" != "$__ALIAS_CACHE" ]; then
    source "$file"
    __ALIAS_CACHE="$new_hash"
  fi
}

8. Dicas de Depuração e Manutenção

8.1. Listando aliases carregados

alias | grep -E "^alias (build|test|run)="

Ou para listar apenas os do projeto atual:

alias -p | grep "$PWD"

8.2. Logging simples

function __log_alias_load() {
  echo "$(date '+%Y-%m-%d %H:%M:%S') [PROJETO] $PWD - Aliases carregados" >> /tmp/project_aliases.log
}

8.3. Remoção segura ao sair do diretório

__unload_project_aliases() {
  if [ -n "$__PROJECT_ALIASES_LOADED" ]; then
    unalias build test run 2>/dev/null
    unset -f deploy lint 2>/dev/null
    unset __PROJECT_ALIASES_LOADED
    echo "[ALIAS] Aliases do projeto descarregados"
  fi
}

Referências