Interface de Linha de Comandos

Nessa aula vamos ver várias maneiras para melhorar o seu ambiente de trabalho ao usar o shell. Já estamos trabalhando com o shell há um tempo, mas focamos principalmente em executar diferentes comandos. Agora vamos ver como executar diferentes processos ao mesmo tempo enquanto observamos o seu funcionamento, como parar ou pausar um processo específico e como fazer um processo funcionar em segundo plano.

Também vamos aprender diferentes maneiras para melhorar o seu shell e outras ferramentas, ao definir apelidos e configurá-las utilizando dotfiles. Ambas ambordagens podem lhe ajudar a poupar tempo, ao por exemplo utilizar a mesma configuração em todas as suas máquinas sem ter que digitar longos comandos. Tambpém vamos ver como trabalhar em máquinas remotas utilizando SSH.

Controle de processos

Em alguns casos você precisará interromper um processo enquanto ele está executando, por exemplo: quando um comando está demorando muito para finalizar a sua execução (como um find que precisará percorrer uma estrutura de diretórios muito grande). Na maioria dos casos, você pode pressionar Ctrl-C e o comando será interrompido. Mas como isso funciona de fato e por que às vezes isso não é suficiente para parar o processo?

Matando um processo

O seu shell está utilzando um mecanismo de comunicação do UNIX chamado sinal para passar informações ao processo. Quando um processo recebe um sinal, ele para a sua execução, interpreta o sinal e potencialmente modifica o seu fluxo de execução baseado na informação passada pelo sinal. Por essa razão, sinais são interruptores de software.

Nesse caso, quando Ctrl-C é pressionado, isso indica ao shell para passar um sinal SIGINT para o processo.

Aqui está um exemplo mínimo de um programa Python que capture um sinal SIGINT e o ignora, não mais interrompendo a sua execução ao recebê-lo. Para matar esse programa nós podemos utilizar o sinal SIGQUIT ao digitar Ctrl-\.

#!/usr/bin/env python
import signal, time

def handler(signum, time):
    print("\nI got a SIGINT, but I am not stopping")

signal.signal(signal.SIGINT, handler)
i = 0
while True:
    time.sleep(.1)
    print("\r{}".format(i), end="")
    i += 1

Isso que acontece se mandarmos um sinal SIGINT duas vezes para esse programa, seguidos de um SIGQUIT. Note que ^ é como Ctrl é mostrado quando digitado no terminal.

$ python sigint.py
24^C
I got a SIGINT, but I am not stopping
26^C
I got a SIGINT, but I am not stopping
30^\[1]    39913 quit       python sigint.py

Enquanto ambos SIGINT e SIGQUIT são normalmente associados com requisições relacionadas ao terminal, um sinal mais genérico para demandar o fim de um processo é o sinal SIGTERM. Para mandar esse sinal podemos utilizar o comando kill, com a sintaxe kill -TERM <PID>.

Pausando e colocando processos em segundo plano

Sinais podem fazer outras coisas além de matar processos. Por exemplo, SIGSTOP pausa um processo. No terminal, pressionar Ctrl-Z irá requisitar ao shell para enviar um sinal SIGTSTP, que é uma abreaviação para Terminal Stop (a versão do terminal para SIGSTOP).

O processo pausado pode ser então continuado em primeiro ou segundo plano utilizando fg ou bg, respectivamente.

O comando jobs lista os processos não finalizados associados com a sessão atual do terminal. Você pode se referir a esses processos utilizando o pid deles (você pode utilizar o comando pgrep para descobrí-los). De maneira mais intuitiva, você também pode referir a um processo utilizando o símbolo de porcentagem seguido do seu número identificador (indicado por jobs). Para se referir ao último processo que foi posto em segundo plano você pode utilizar o parâmetro especial $!.

Outra coisa importante é que utilizar o sufixo & em um comando vai executá-lo em segundo plano, lhe dando o controle do terminal de volta. No entanto, esse comando ainda vai utilizar a saída padrão (STDOUT) do shell, o que pode incomodar (você pode utilizar redirecionamentos do shell nesse caso).

Para colocar em segundo plano um processo que já está rodando você pode pressionar Ctrl-Z, e executar o comando bg em seguida. Note que processos em segundo plano ainda são processos filhos do seu terminal e vão morrer se você fechá-lo (isso enviará um outro sinal, SIGHUP). Para evitar que isso aconteça você pode executar o programa com nohup (um wrapper para ignorar SIGHUP), ou utilizar disown se o processo já foi iniciado. Como outra alternativa, você pode utilizar um multiplexador de terminais, como veremos na próxima seção.

Abaixo é demonstrada uma sessão de exemplo para ilustrar alguns desses conceitos.

$ sleep 1000
^Z
[1]  + 18653 suspended  sleep 1000

$ nohup sleep 2000 &
[2] 18745
appending output to nohup.out

$ jobs
[1]  + suspended  sleep 1000
[2]  - running    nohup sleep 2000

$ bg %1
[1]  - 18653 continued  sleep 1000

$ jobs
[1]  - running    sleep 1000
[2]  + running    nohup sleep 2000

$ kill -STOP %1
[1]  + 18653 suspended (signal)  sleep 1000

$ jobs
[1]  + suspended (signal)  sleep 1000
[2]  - running    nohup sleep 2000

$ kill -SIGHUP %1
[1]  + 18653 hangup     sleep 1000

$ jobs
[2]  + running    nohup sleep 2000

$ kill -SIGHUP %2

$ jobs
[2]  + running    nohup sleep 2000

$ kill %2
[2]  + 18745 terminated  nohup sleep 2000

$ jobs

Um sinal especial é o SIGKILL, porque ele não pode ser capturado pelo processo e sempre o terminará imediatamente. No entanto, isso pode ter efeitos colaterais indesejados, como originar processos órfãos.

Você pode aprender mais sobre esses e outros sinais aqui, executando o comando man signal, ou kill -t.

Multiplexadores de terminais

Ao usar a interface de linha de comando você frequentemente vai querer executar mais de uma coisa ao mesmo tempo. Por exemplo, você pode querer executar um editor e um programa lado a lado. Apesar de isso poder ser feito ao abrir novas janelas de terminal, utilizar um multiplexador de terminais é uma solução mais versátil.

Multiplexadores de terminais como o tmux lhe permitem mutiplexar janelas de terminais utilizando painéis e abas de maneira que você possa interagir com diferentes shells em diferentes sessões. Ademais, os multiplexadores de terminais permitem que você desmonte uma sessão atual e a monte novamente em algum momento futuro. Isso pode melhorar muito o seu fluxo de trabalho ao trabalhar com máquinas remotas, pelo fato de evitar a necessidade de utilizar o comando nohup e outros truques semelhantes.

O multiplexador de terminais mais popular hoje em dia é o tmux. O tmux é altamente configurável e ao utilizar os seus atalhos do teclado você pode criar múltiplas abas e painéis e navegar rapidamente entre eles.

O tmux espera que você conheça os seus atalhos do teclado. Todos eles tem o formato <C-b> x, que isso significa (1) pressionar Ctrl+b, (2) soltar Ctrl+b, e então (3) pressionar x. O tmux tem a seguinte hierarquia de objetos:

Para referência, aqui está um tutorial rápido do tmux e aqui você pode ver uma explicação mais detalhada que cobre o comando original screen. Talvez você também queira se familiarizar ao screen, já que ele vem instalado na maioria dos sistemas UNIX.

Apelidos

Escrever longos comandos que envolvem muitos parâmetros e opções prolixas pode ser cansativo. Por essa razão, muitos shells permitem a criação de apelidos. Um apelido do shell é uma representação mais curta para outro comando que o shell irá substituir automaticamente para você. Por exemplo, um apelido no bash tem a seguinte estrutura:

alias nome_do_apelido="comando_abreviado arg1 arg2"

Observe que não há espaços em volta do sinal de igual =, porque alias é um comando que recebe um único argumento.

Apelidos tem várias funcionalidades convenientes:

# Fazer atalhos para parâmetros comuns
alias ll="ls -lh"

# Digitar menos em comandos comuns
alias gs="git status"
alias gc="git commit"
alias v="vim"

# Driblar problemas causados por erros de digitação
alias sl=ls

# Sobrescrever comandos existents para um melhor comportamento padrão
alias mv="mv -i"           # -i perguntar antes de sobreescrever
alias mkdir="mkdir -p"     # -p cria diretórios pais conforme necessário
alias df="df -h"           # -h imprime em formáto legível para humanos

# Apelidos podem ser compostos
alias la="ls -A"
alias lla="la -l"

# Para ignorar um apelido execute o comando prefixado por um \
\ls
# Ou desative o apelido com o comando `unalias`
unalias la

# Para obter a definição de um apelido chame-o com o comando `alias`
alias ll
# Imprimirá ll='ls -lh'

Note também que apelidos não persistem por sessões do shell por padrão. Para fazer que um apelido seja persistente, você precisa incluí-lo em arquivos de inicialização do shell, como .bashrc ou .zshrc, que vão ser introduzidos na próxima seção.

Dotfiles

Muitos programas são configurados utilizando arquivos de texto simples conhecidos como dotfiles. (porque os nomes desses arquivos começam com um ., como por exemplo ~/.vimrc, ficando ocultos na listagem de diretórios do ls por padrão.)

Shells são exemplos de programas configurados com esses arquivos. Na sua inicialização, o seu shell irá ler vários arquivos para carregar a sua configuração. Dependendo do shell, seja iniciando um login e/ou uma sessão interativa o processo de inicialização pode ser razoavelmente complexo. Depending on the shell, whether you are starting a login and/or interactive the entire process can be quite complex. Aqui está uma excelente referência nesse tópico.

No caso do bash, editar o seu .bashrc ou .bash_profile funcionará na maioria dos sistemas. Aqui você pode incluir comandos que você deseja que rodem na inicialização, como os apelidos descritos na seção anterior ou modificações à variável de ambiente PATH. Na verdade, vários programas pedirão que você inclua uma linha como export PATH="$PATH:/caminho/para/programa/bin" na configuração do seu shell para que os seus arquivos binários possam ser encontrados.

Some other examples of tools that can be configured through dotfiles are: Outros exemplos de ferramentas que podem ser configuradas por meio de dotfiles são:

Como dotfiles devem ser organizados? Eles devem estar na sua própria pasta, sob um sistema de controle de versionamento e com links simbólicos criados adequadamente utilizando um script. Isso tem os seguintes benefícios:

O que deve ser colocado nos seus dotfiles? Você pode aprender mais sobre as configurações de uma ferramenta específica ao ler a sua documentação online ou as páginas do man. Outra ótima alternativa é pesquisar na internet por posts em blogs sobre programas específicos, em que autores expõe as suas customizações preferidas. Outra maneira para aprender sobre essas customizações é olhar os dotfiles de outras pessoas: você pode encontrar diversos repositórios de dotfiles no GitHub — veja o mais popular aqui (no entanto, recomenda-se que você não copie configurações cegamente. Tente adaptar às suas preferências). Aqui você pode encontrar outra boa referência nesse tópico.

Todos os instrutores das aulas tem os seus dotfiles disponibilizados no GitHub: Anish, Jon, Jose.

Portabilidade

Um problema comum com dotfiles é que as suas configurações podemo não funcionar quando se trabalha em diferentes máquinas, como por exemplo se essas máquinas tiverem diferentes sistemas operacionais ou shells. Às vezes você também pode querer que determinada configuração seja aplicada somente a uma determinada máquina.

Existem alguns truques para que isso seja mais fácil de se realizar. Se o arquivo de configuração dá suporte a isso, use o equivalentea expressões if-else para aplicar customizações para máquinas especificas. Por exemplo, o seu shell pode ter algo como o seguinte:

if [[ "$(uname)" == "Linux" ]]; then {faca_algo}; fi

# Check before using shell-specific features
if [[ "$SHELL" == "zsh" ]]; then {faca_algo}; fi

# You can also make it machine-specific
if [[ "$(hostname)" == "meuServidor" ]]; then {faca_algo}; fi

Se o arquivo de configuração der suporte, você pode utilizar includes. Por exemplo, um arquivo ~/.gitconfig pode ter a seguinte configuração:

[include]
    path = ~/.gitconfig_local

Então em cada máquina, o arquivo ~/.gitconfig_local pode conter configurações especificas de cada máquina. Você pode até gerenciar esses arquivos em um repositório separado para configurações específicas.

Essa ideia também é útil caso você queira que diferentes programas compartilhem algumas configurações. Por exemplo, se você quiser que ambos os shells zsh e bash compartilhem o mesmo conjunto de apelidos você pode escrevê-los em um arquivo .aliases e ter o seguinte bloco em ambos:

# Testa se ~/.aliases existe e o carrega
if [ -f ~/.aliases ]; then
    source ~/.aliases
fi

Máquinas Remotas

É cada vez mais comum para programadores utilizar servidores remoto no dia-a-dia. Se você precisa utilzar servidores remotos para realizar a implantação de um software backend ou se você de um servidor com um maior poder computacional, você provavelmente utilizará Secure Shell (SSH). Assim como a maioria das ferramentas aqui demonstradas, SSH é altamente configurável, então vale a pena aprender mais sobre isso.

Para entrar com ssh em um servidor você precisa executar o seguinte comando

ssh foo@bar.mit.edu

Aqui estamos tentando entrar com SSH utilizando o usuário foo no servidor bar.mit.edu. O servidor pode ser especificado com uma URL (como bar.mit.edu) ou um IP (algo como foobar@192.168.1.42). Depois veremos como modificar o arquivo de configuração do SSH para acessar um servidor apenas digitando algo como ssh bar.

Executando comandos

Uma funcionalidade do ssh frequentemente deixada de lado é a possibilidade de se executar comandos diretamente. ssh foobar@server ls executar ls na pasta raíz de foobar. Isso funciona com pipes, então ssh foobar@server ls | grep PATTERN irá executar o comando grep localmente sobre a saída remota de ls e ls | ssh foobar@server grep PATTERN rodará grep remotamente para a saída local de ls.

Chaves SSH

Autenticação baseada em chaves utiliza criptografia de chaves privadas para provar ao servidor que o cliente possui a chave privada secreta sem revelar a chave. Dessa maneira, você não precisa reintroduzir a sua senha sempre. De qualquer jeito, a chave privada (frequentemente ~/.ssh/id_rsa e mais recentemente ~/.ssh/id_ed25519) é efetivamente a sua senha, então trate-a como tal.

Geração de chaves

Para gerar um pair de chaves você pode executar ssh-keygen.

ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519

Você precisa escolher uma senha, para evitar que alguém que obtenha a sua chave privada consiga acessar servidores autorizados. Utilize ssh-agent ou gpg-agent para que você não precise digitar a sua senha sempre.

Se alguma vez você configurou a atualização de repositórios do GitHub utilizando chaves SSH, então você provavelmente seguiu os passos listados aqui aqui e já tem um par de chaves. Para checar se voc6e tem uma senha e validá-la você pode executar ssh-keygen -y -f /caminho/para/chave.

Autenticação baseada em chaves

ssh checará .ssh/authorized_keys para determinar a que clientes deverá permitir o acesso. Para copiar para esse caminho uma chave pública você pode utilizar: ssh will look into .ssh/authorized_keys to determine which clients it should let in. To copy a public key over you can use:

cat .ssh/id_ed25519.pub | ssh foobar@remote 'cat >> ~/.ssh/authorized_keys'

Uma solução mais simples pode ser alcançada com ssh-copy-id, quando disponível:

ssh-copy-id -i .ssh/id_ed25519.pub foobar@remote

Copiando arquivos por SSH

Existem diversas maneiras para copiar arquivos por SSH:

Redirecionamento de portas

Em muitos cenários estarão presentes softwares que funcionam em uma determinada porta da máquina. Quando isso acontece na sua máquina local você pode digitar localhost:PORTA ou 127.0.0.1:PORTA, mas o que você faz quando um servidor remoto não tem suas portas diretamente disponíveis pela rede/internet?

Isso é chamado de redirecionamento de portas e isso pode ser realizado de duas maneiras: Redirecionamento Local de Portas e Redirecionamento Remoto de Portas (veja as figuras em mais detalhes, créditos para as figuras desse post do StackOverflow).

Redirecionamento Local de Portas Redirecionamento Local de Portas Redirecionamento Remoto de Portas Redirecionamento Remoto de Portas

O cenário mais comum é o redirecionamento local de portas, em que um serviço na máquina remota executa em uma porta e você quer vincular uma porta na sua máquina local para redirecionar para a porta remota. Por exemplo, considere o caso em que executamos jupyter notebook no servidor remoto na porta 8888. Então, para redirecionar para a porta local 9999, poderíamos executar ssh -L 9999:localhost:8888 foobar@servidor_remoto e então navegar para localhost:9999 na nossa máquina local.

Configuração SSH

Aqui cobrimos muitos argumentos que pode sem passados para o ssh. Uma alternatvia tentadora é criar apelidos do shell, algo parecido com:

alias meu_servidor="ssh -i ~/.id_ed25519 --port 2222 -L 9999:localhost:8888 foobar@servidor_remoto

No entanto, existe uma alternativa melhor utilizando ~/.ssh/config.

Host vm
    User foobar
    HostName 172.16.174.141
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    LocalForward 9999 localhost:8888

# Configurações podem também possuir "wildcards"
Host *.mit.edu
    User foobaz

Uma vantagem adicional de utilizar o arquivo ~/.ssh/config ao invés de apelidos é que outros programas como scp, rsync, mosh, etc podem lê-lo assim como converter as configurações nas flags correspondentes.

Note que o arquivo ~/.ssh/config pode ser considerado um dotfile, e em geral não há problema de incluí-lo junto com o restante dos seus dotfiles. No entanto, se você deixá-lo disponível publicamente, pense sobre a informação que você está potencialmente disponibilizando a estranhos na internet: endereços dos seus servidores, usuários, portas abertas, etc. Isso pode facilitar tipos de ataques, então pense bem antes de compartilhar a sua configuração do SSH.

Configuracões do lado do servidor são geralmente especificadas em /etc/ssh/sshd_config. Aqui você pode fazer mudanças como desativar autenticação por senha, modificar portas ssh, ativar redirecionamento X11, etc. Você também pode especificar configurações para cada usuário.

Diversos

Um problema comum em conexões a servidores remotos são desconexões em função do desligamento/modo de descanso do seu computador ou modificação da rede. Além disso, acesso ssh em uma conexão com latência significante pode ser bem frustrante. Mosh, o shell móvel, trás melhoras sobre o ssh, ao permitir conexões em roaming, conectividade intermitente e disponibilizando eco local inteligente.

Às vezes é conveniente montar um diretório remoto. sshfs pode montar localmente diretórios em um servidor remoto, fazendo com que você possa utilizá-lo com um editor local. locally, and then you can use a local editor.

Shells e Frameworks

Ao falar sobre ferramentas de shell e de scripts tratamos o shell bash porque esse é de longe o shell mais onipresente e a maioria dos sistemas o tem como a opção padrão. No entanto, essa não é a única opção.

Por exemplo, o zsh é uma extensão do bash e assim fornece por padrão várias funcionalidades úteis como:

Frameworks podem melhorar o seu shell também. Alguns frameworks gerais são o prezto e o oh-my-zsh, e alguns menores, focados em funcionalidades específicas como o zsh-syntax-highlighting e o zsh-history-substring-search. Shells como o fish incluem várias dessas funcionalidades amigáveis por padrão. Algumas delas são:

Algo a se notar quando se utiliza frameworks como esse é que eles podem deixar o seu shell mais lento, especialmente se o código que eles rodam não é otimizado propriamente ou se é muito código em si. Você pode analisar isso e desabilitar funcionalidades que você não utiliza muito ou que estejam deixando o seu shell mais lento.

Emuladores de terminais

Além de customizar o seu shell, também pode ser útil avaliar o uso de diferentes emuladores de terminais e as suas diferentes configurações. Existem diferentes emuladores de terminais disponíveis para download e uso (aqui pode ser vista uma comparação, em inglês).

Já que você pode passar centenas ou até milhares de horas no seu terminal, pode ser que valha a pena explorar as suas configurações. Alguns aspectos que você pode querer modificar nele incluem:

Exercícios

Controle de processos

  1. Pelo que já vimos, podemos utilizar comandos do tipo ps aux | grep para obter os pids de processos e então encerrá-los, mas existem melhores maneiras de se fazer isso. Comece rodando o comando sleep 10000 em um terminal, ponha ele em segundo plano pressionando Ctrl-Z e continue a sua execução com bg. Agora utilize pgrep para achar o seu pid e pkill para encerrá-lo sem precisar digitar o seu pid. (Dica: utilize as flags -af).

  2. Vamos supor que você não quer começar um processo até que outro termine. Como você faria isso? Nesse exercício o nosso processo limitador sempre será sleep 60 &. Um jeito de fazer isso é utilizar o comando wait. Tente rodar o comando sleep 60 & e fazer com que um comando ls espere o processo em segundo plano finalizar. No entanto, essa estratégia falhará se o iniciarmos em uma sessão de um bash diferente, já que wait só considera processos filhos. Uma funcionalidade que não discutimos foi que o status de saída do comando kill é zero no caso de sucesso e um número diferente de zero caso contrário. kill -0 não manda um sinal mas dará um status diferente de zero se o processo não existir. Escreva uma função bash chamada pidwait que recebe um pid e espera até que o processo dado complete. Você deverá utilizar o comando sleep para evitar o gasto desnecessário de ciclos de CPU.

Multiplexador de terminal

  1. Siga este tutorial do tmux e aprenda como fazer algumas combinações básicas seguindo [esses passos] (https://www.hamvocke.com/blog/a-guide-to-customizing-your-tmux-conf/).

Apelidos

  1. Crie um apelido chamado dc que executa o comando cd quando você digitá-lo incorretamente.

  2. Execute history | awk '{$1="";print substr($0,2)}' | sort | uniq -c | sort -n | tail -n 10 para listar os seus 10 comandos mais executados e considere escrever apelidos mais curtos para eles. Nota: isso apenas funciona para o Bash; se você estiver utilizando ZSH, utilize o comando history 1 ao invés de apenas history.

Dotfiles

Vamos praticar o uso de dotfiles.

  1. Crie uma pasta para os seus dotfiles e configure seu sistema de controle de versionamento.
  2. Adicione uma configuração para pelo menos um programa, como por exemplo o seu shell, com algumas customizações (por exemplo, pode ser algo simples como customizar o prompt do seu ao atribuir um valor para $PS1).
  3. Configure um método para instalar os seus dotfiles rapidamente (e sem esforço manual) em uma nova máquina. Isso pode ser tão simples quando um script shell que chama ln -s para cada arquivo, ou você pode utilizar um utilitário especializado.
  4. Teste o seu script de instalação em uma máquina virtual recém instalada.
  5. Migre todas as suas configurações atuais para o seu repositório de dotfiles.
  6. Publique os seus dotfiles no GitHub.

Máquinas Remotas

Instale uma máquina virtual Linux (ou utilize uma existente) para este exercício. Se você não tem familiaridade com máquinas virtuais, veja esse tutorial para aprender como instalar uma.

  1. Navegue até a pasta ~/.ssh/ e verifique se existe um par de chaves SSH nela. Se não, gere um com o comando ssh-keygen -o -a 100 -t ed25519. É recomendado que você utilize uma senha e utilize ssh-agent, (mais informação aqui).
  2. Edite o arquivo .ssh/config para adicionar uma entrada como a seguinte:
Host vm
    User usuario_vai_aqui
    HostName ip_vai_aqui
    IdentityFile ~/.ssh/id_ed25519
    LocalForward 9999 localhost:8888
  1. Utilize ssh-copy-id maquina-virtual para copiar a chave SSH para o servidor.
  2. Inicie um servidor na sua máquina virtual executando python -m http.server 8888. Acesse esse servidor ao navegar ao endereço http://localhost:9999 na sua máquina.
  3. Edite a configuração do seu servidor SSH executando sudo vim /etc/ssh/sshd_config e desabilite a autenticação por senha ao editar o valor de PasswordAuthentication. Desabilite login do usuário root ao editar o valor de PermitRootLogin. Reinicie o serviço ssh executando sudo service sshd restart. Tente entrar no servidor com SSH novamente.
  4. (Desafio) Instale o mosh na máquina virtual e abra uma conexão com ele. Depois, desconecte o adaptador de rede do servidor/máquina virtual. O mosh consegue se recuperar dessa falha?
  5. (Desafio) Procure saber o que as flags -N e -f fazem no comando ssh e descubra qual comando conseguiria realizar o redirecionamento de portas em segundo plano.

Edite esta página.

Licensed under CC BY-NC-SA.