Os comandos shell script fazem parte da caixa de ferramentas de muitos programadores e são praticamente indispensáveis para aqueles que desejam trabalham com a máxima produtividade em ambientes Linux / Unix.

Por isso, muitos gostariam de poder executar esses comandos dentro de seus programas Java. Veremos nesse artigo como realizar essa atividade, tanto a nível local como remotamente.

Executando comandos locamente

Para executar comandos shell script em máquinas locais, utilizaremos a classe ProcessBuilder, introduzida no java 1.5.


import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.logging.Logger;

public class LocalShell {

    private static final Logger log = Logger.getLogger(LocalShell.class.getName());    

    public void executeCommand(final String command) throws IOException {
        
        final ArrayList<String> commands = new ArrayList<String>();
        commands.add("/bin/bash");
        commands.add("-c");
        commands.add(command);
        
        BufferedReader br = null;        
        
        try {                        
            final ProcessBuilder p = new ProcessBuilder(commands);
            final Process process = p.start();
            final InputStream is = process.getInputStream();
            final InputStreamReader isr = new InputStreamReader(is);
            br = new BufferedReader(isr);
            
            String line;            
            while((line = br.readLine()) != null) {
                System.out.println("Retorno do comando = [" + line + "]");
            }
        } catch (IOException ioe) {
            log.severe("Erro ao executar comando shell" + ioe.getMessage());
            throw ioe;
        } finally {
            secureClose(br);
        }
    }
    
    private void secureClose(final Closeable resource) {
        try {
            if (resource != null) {
                resource.close();
            }
        } catch (IOException ex) {
            log.severe("Erro = " + ex.getMessage());
        }
    }
    
    public static void main (String[] args) throws IOException {
        final LocalShell shell = new LocalShell();
        shell.executeCommand("ls ~");
    }
}
Listagem 1. Código para executar comandos shell localmente

Ao rodar essa classe, ela irá executar o comando ls ~, que listará todos os diretórios e arquivos da minha pasta home.


Retorno do comando = [a.txt]
Retorno do comando = [Desktop]
Retorno do comando = [Documents]
Retorno do comando = [Downloads]
Retorno do comando = [examples.desktop]
Retorno do comando = [Music]
Retorno do comando = [NetBeansProjects]
Retorno do comando = [Pictures]
Retorno do comando = [Public]
Retorno do comando = [Templates]
Retorno do comando = [teste.txt]
Retorno do comando = [Videos]
Retorno do comando = [z.txt]
BUILD SUCCESSFUL (total time: 0 seconds) 
Listagem 2. Resultado da execução do programa

Nota: Essa classe foi rodada no Ubuntu 12.04, usando Netbeans 7.0.1 e Java 6. Para aqueles que têm apenas o Windows instalado, podem usar o WMware Player para instalar o Linux de sua preferência, como uma máquina virtual. O WMware Player é muito simples de configurar.

A classe ProcessBuilder aceita parâmetros do tipo vargs (arg1, arg2,....), ou através de List. Note que não basta apenas executar o comando ls ~, é necessário antes incluirmos o seguinte comando: /bin/bash -c. Isso se deve porque todo comando precisa ser executado em um shell, e no caso estamos determinando que o shell bash processe o comando (Formato da linha de comando: /bin/bash -c ).

Podemos ainda, recuperar a saída do comando passado como argumento (caso ele gere uma saída), através do método getInputStream().

Através da classe ProcessBuilder, podemos então executar vários comandos linux e até script shells, tornando-a extremamente valiosa para implementar automação de tarefas, processamento de texto e outras atividades feitas através de script shells.

Executando comandos remotamente

Obviamente, não podemos utilizar a classe LocalShell para executar comandos remotos em outras máquinas Linux. Temos uma série de fatores que devem ser equacionados para que isso seja possível, a começar pela comunicação de rede e conexão segura.

Geralmente, usuários Linux utilizam o aplicativo SSH, para estabelecer conexão remota com outras máquinas, e assim podem executar comandos no shell.

Felizmente, é possível utilizar a mesma abordagem com Java, através de um cliente SSH embutido, que irá gerenciar toda a parte de conexão e segurança, possibilitando a execução de comandos remotos.

No GitHub, existe um projeto chamado sshj, criado pelo user: shikhar. Ele contém, entre outras coisas, um cliente de ssh.

Iremos criar então no Netbeans um projeto utilizando essa API. (Desta vez o projeto será criado no Windows, para demonstrar que o sshj pode ser usado em outro SO).

Procedimentos:

  • O link de download para o framework: sshj;
  • Baixe a versão 0.8.1 (sshj-0.8.1.zip);
  • Descompacte o arquivo zip;
  • Localizar o arquivo jar sshj-0.8.1.jar.

Esse jar depende de outros dois frameworks:

Nesse projeto foram configurados os seguintes jars:

Bibliotecas do projeto
Figura 1. Bibliotecas do projeto

Acrescente então, as classes da Listagem 3 e 4.


package br.com.devmedia.ssh;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.PublicKey;
import java.util.concurrent.TimeUnit;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.connection.ConnectionException;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.connection.channel.direct.Session.Command;
import net.schmizz.sshj.transport.TransportException;
import net.schmizz.sshj.transport.verification.HostKeyVerifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RemoteShell {

    private final Logger log = LoggerFactory.getLogger(RemoteShell.class);
    private final String machine;
    private final String user;
    private final String password;
    
    public RemoteShell(final String machine, final String user, final String password) {
        this.machine = machine;
        this.user = user;
        this.password = password;
    }

    public void executeCommand(final String command) throws IOException {

        // Cliente SSH
        final SSHClient ssh = new SSHClient();

        try {
            // Configura tipo de KeyVerifier
            setupKeyVerifier(ssh);
            // Conecta com a maquina remota
            ssh.connect(machine);
            // Autenticacao
            ssh.authPassword(user, password);

            // Executa comando remoto
            executeCommandBySSH(ssh, command);
        } finally {
            ssh.disconnect();
        }
    }

    private void executeCommandBySSH(final SSHClient ssh, final String command) 
    throws ConnectionException, IOException, TransportException {
        
        final Session session = ssh.startSession();
        BufferedReader bf = null;

        try {
            // Executa comando
            final Command cmd = session.exec(command);
            bf = new BufferedReader(new InputStreamReader(cmd.getInputStream()));
            String line;
            // Imprime saida, se exister
            while ((line = bf.readLine()) != null) {
                System.out.println(line);
            }
            // Aguarda
            cmd.join(1, TimeUnit.SECONDS);
        } finally {
            secureClose(bf);
            secureClose(session);
        }
    }

    private void setupKeyVerifier(final SSHClient ssh) {
        ssh.addHostKeyVerifier(
                new HostKeyVerifier() {
                    @Override
                    public boolean verify(String arg0, int arg1, PublicKey arg2) {
                        return true;  // sem verificacao 
                    }
                });
    }

    private void secureClose(final Closeable resource) {
        try {
            if (resource != null) {
                resource.close();
            }
        } catch (IOException ex) {
            log.error("Erro ao fechar recurso", ex);
        }
    }
}
Listagem 3. Classe RemoteShell

package br.com.devmedia.ssh;

import java.io.IOException;

public class TestRemoteShell {
    
    public static void main(String... args) throws IOException {        
        final RemoteShell shell = new RemoteShell("<IP>", "<user>", "<password>");
        shell.executeCommand("ls ~ | sort");        
    }
}
Listagem 4. Classe de Teste

a.txt
Desktop
Documents
Downloads
examples.desktop
Music
NetBeansProjects
Pictures
Public
Templates
teste.txt
Videos
z.txt
Listagem 5. Saída da execução

Podemos usar um comando mais sofisticado. Por exemplo, procurar todos os arquivos .txt, e executar o cksum neles. Para isso, use o comando a seguir: find ~ -name '*.txt' -exec cksum {} \\; | sort -k3


4294967295 0 /home/senaga/a.txt
4294967295 0 /home/senaga/.config/libreoffice/3/user/uno_packages/cache/log.txt
435791989 154 /home/senaga/.mozilla/firefox/jv93gx4x.default/urlclassifierkey3.txt
2918312305 327 /home/senaga/.netbeans/7.0/var/cache/lastModified/all-checksum.txt
4294967295 0 /home/senaga/teste.txt
4294967295 0 /home/senaga/z.txt
Listagem 6. Saída da execução para o comando find

A Classe RemoteShell utiliza a SSHClient, que provê toda a infraestrutura necessária para realizar a conexão, autenticação e execução de comandos remotos via protocolo SSH através de métodos bem intuitivos.

O maior ponto de atenção é o método setupKeyVerifier, onde definimos uma classe interna anônima que implementa a interface HostKeyVerifier, para o método addHostKeyVerifier. Para entender o processo de Host Key Verifier, devemos ter em mente como funciona o SSH.

Quando utilizamos o cliente ssh pela primeira vez para conexão ao servidor ssh, ele irá verificar a chave do servidor no arquivo ~/.ssh/known_hosts. Essa chave é conhecida como fingerprint e possuem o seguinte formato: 43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8 (exemplo).

Toda vez que conectamos nesse servidor, o cliente ssh irá verificar se a chave é a mesma. Caso ela mude, o cliente pode emitir um aviso de alerta ou abortar a conexão.

Ao acrescentar a classe anônima:


ssh.addHostKeyVerifier(
new HostKeyVerifier() {
    @Override
    public boolean verify(String arg0, int arg1, PublicKey arg2) {
        return true;  // sem verificacao 
    }
});
Listagem 7. Verificando a chave

Estamos dizendo ao SSHClient que não efetue nenhum tipo de validação para as chaves. Isso faz sentido ao rodar essa aplicação no Windows, onde geralmente não temos o arquivo known_hosts disponível. Já no Linux, para maior segurança, pode-se eliminar totalmente o método setupKeyVerifier, e utilizar essa função:


client.loadKnownHosts();
Listagem 8. Função loadKnownHosts

O método irá carregar as fingerprints do arquivo ~/.ssh/known_hosts e irá impedir a conexão se o fingerprint mudar.

Espero que esse artigo tenha sido útil para auxiliar os desenvolvedores a criarem ferramentas de automação utilizando shell script, localmente ou remotamente, através da poderosa linguagem Java.

Referências: