Neste artigo veremos como usar a classe Runtime para construir um emulador de terminal linux. Nosso projeto será construído com base nos seguintes requisitos:

  • Possibilitar ao usuário executar comandos no emulador de terminal e obter a saída destes;
  • Possibilitar a visualização de históricos dos comandos já executados, possibilitando a escolha de um destes para execução;
  • Mostrar erros de saída caso o comando executado seja inválido, colorindo a saída de vermelho quando ERRO e preto quando executado corretamente;
  • Possibilitar a execução dos comandos através da tecla de atalho F5 para agilizar e facilitar o uso;
  • Possibilitar a limpeza dos históricos dos comandos já executados.

Nosso projeto consistirá de três Classes, que são:

  • TerminalEmulator: Será a classe “core” responsável por executar os comandos no terminal real do Linux, capturando seus retornos e realizando os tratamentos necessários;
  • Fterminal: Formulário que fará a interface entre o usuário e a classe TerminalEmulator;
  • Dhistory: Mostrará os históricos de comandos já executados possibilitando a seleção e re-execução de qualquer comando a qualquer momento.

TerminalEmulator

Vamos começar nosso estudo através da classe que de fato faz todo trabalho árduo, executando os comandos e cuidando dos históricos de comando.

Vejamos ela por completa na Listagem 1.


  package br.com.lab;
   
  import java.io.BufferedReader;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
   
  public class TerminalEmulator {
           
    private static List<String> commandHistory = new ArrayList<String>();

    public static Map<String, String> exec(String command) {
       Process proc = null;
       Map<String, String> result = new HashMap<String, String>();
       try {
          proc = Runtime.getRuntime().exec(command);

          result.put("input", inputStreamToString(proc.getInputStream()));
          result.put("error", inputStreamToString(proc.getErrorStream()));                         

       } catch (IOException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
          result.put("error", e.getCause().getMessage());  
       } finally{
          commandHistory.add(command);
       }

       return result;
    }

    private static String inputStreamToString(InputStream isr) {

       try {

          BufferedReader br = new BufferedReader(new InputStreamReader(isr));
          StringBuilder sb = new StringBuilder();
          String s = null;

          while ((s = br.readLine()) != null) {
                    sb.append(s + "\n");
          }

          return sb.toString();

       } catch (IOException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
          return null;
       }
    }

    public static void clearHistory(){
       commandHistory.clear();
    }

    public static List<String> getCommandHistory() {
       return commandHistory;
    }
  }
  
Listagem 1. TerminalEmulator.java

Toda vez que um comando for executado, seja ele errado ou certo, ele será gravado na lista 'commandHistory':


private static List<String> commandHistory = new ArrayList<String>();

O método getCommandHistory() possibilita que o histórico de comandos possa ser recuperado de outros pontos do sistema e o método clearHistory() faz a limpeza de todos os históricos. Perceba que este possui a palavra reservada “static”, ou seja, seu estado será mantido enquanto a aplicação estiver em execução.

O método mais importante de todos é o “exec()”, pois é ele quem vai executar o comando no terminal real, ou seja, o terminal do Linux. O exec() retorna um Map contendo sempre a chave “input” e a chave “error”. A chave “input” contém o conteúdo retornando quando a execução for bem-sucedida, enquanto a chave “error” retorna o conteúdo de erro, quando houver.

A linha que faz tudo funcionar é:


proc = Runtime.getRuntime().exec(command);

Esta captura a instância de Runtime e chama o método “exec()”, passando o comando desejado (ex: 'ls -la'). O seu retorno é um objeto do tipo “Process”, e é através deste objeto que capturaremos o retorno caso sucesso ou erro.

Nas linhas a seguir chamamos o método inputStreamToString que faz a conversão de InputStream para String (explicaremos mais à frente):


result.put("input", inputStreamToString(proc.getInputStream()));
  result.put("error", inputStreamToString(proc.getErrorStream()));

O objeto Process possui o getInputStream e getErrorStream, onde o primeiro possui o conteúdo de retorno quando sucesso, e o segundo possui o conteúdo de erro quando falha.

Se não houver falhas, então o valor da chave “error” ficará vazia, porém, se houver erro o valor da chave “input” ficará vazio. Se ocorrer um erro interno de IOException gravamos a causa dele na chave “error” do nosso Map:


 catch (IOException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
     result.put("error", e.getCause().getMessage());       
  }

Com isso garantimos que o usuário que estará usando o Emulador de Terminal saberá o que está ocorrendo e que seu comando não foi executado.

O histórico será gravado independente se houve ou não erro, por isso colocamos ele no bloco finally:


 finally{
    commandHistory.add(command);
  }

O método inputStreamToString() converte um objeto do tipo InputStream para String. Para que isso seja possível é criado um InputStreamReader e posteriormente um BufferedReader. É com o BufferedReader que fazemos a leitura das linhas através do “readLine()”.

Fterminal

Dada a explicação necessária para uso do TerminalEmulator.java podemos começar a construção do nosso formulário Fterminal, que fará a “ponte” entre o usuário e o TerminalEmulator. Ele será igual a Figura 1.

Fterminal
Figura 1. Fterminal

Nosso formulário tem dois JtextArea, sendo o primeiro servidor como console para digitação de comandos e o segundo para saída dos comandos. Chamamos de “jTextAreaTerminal” e “jTextAreaOutput”, respectivamente.

Vejamos nossa classe Fterminal por completo na Listagem 2.


package br.com.lab;
 
import java.awt.Color;
import java.awt.event.KeyEvent;
import java.util.Map;
 
import javax.swing.JOptionPane;
 
 
public class FTerminal extends javax.swing.JFrame {
 
    /**
          *
          */
         private static final long serialVersionUID = 1L;
         /**
     * Creates new form FTerminal
     */
    public FTerminal() {
        initComponents();
    }
 
    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">
    //GEN-BEGIN:initComponents
    private void initComponents() {
 
        jButtonExec = new javax.swing.JButton();
        jScrollPane1 = new javax.swing.JScrollPane();
        jTextAreaTerminal = new javax.swing.JTextArea();
        jScrollPane2 = new javax.swing.JScrollPane();
        jTextAreaOutput = new javax.swing.JTextArea();
        jButtonHistory = new javax.swing.JButton();
 
        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
 
        jButtonExec.setText("Executar");
        jButtonExec.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButtonExecActionPerformed(evt);
            }
        });
 
        jTextAreaTerminal.setBackground(new java.awt.Color(216, 208, 201));
        jTextAreaTerminal.setColumns(20);
        jTextAreaTerminal.setRows(5);
        jTextAreaTerminal.addKeyListener(new java.awt.event.KeyAdapter() {
            public void keyPressed(java.awt.event.KeyEvent evt) {
                jTextAreaTerminalKeyPressed(evt);
            }
        });
        jScrollPane1.setViewportView(jTextAreaTerminal);
 
        jTextAreaOutput.setEditable(false);
        jTextAreaOutput.setColumns(20);
        jTextAreaOutput.setRows(5);
        jScrollPane2.setViewportView(jTextAreaOutput);
 
        jButtonHistory.setText("Histórico");
        jButtonHistory.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButtonHistoryActionPerformed(evt);
            }
        });
 
        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
        layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addGroup(layout.createSequentialGroup()
        .addContainerGap()
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(jScrollPane1)
            .addGroup(layout.createSequentialGroup()
                .addComponent(jButtonExec)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jButtonHistory)
                .addGap(0, 0, Short.MAX_VALUE))
            .addComponent(jScrollPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 
            528, Short.MAX_VALUE))
        .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.
            createSequentialGroup()
                .addContainerGap()
                .addComponent(jScrollPane1, javax.swing.GroupLayout.
                PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.
                PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.
                RELATED)
                .addComponent(jScrollPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 134, 
                Short.MAX_VALUE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.
                Alignment.BASELINE)
                    .addComponent(jButtonHistory)
                    .addComponent(jButtonExec))
                .addGap(14, 14, 14))
        );
 
        pack();
    }// </editor-fold>//GEN-END:initComponents
 
    private void jButtonHistoryActionPerformed(java.awt.event.ActionEvent evt) 
    {//GEN-FIRST:event_jButtonHistoryActionPerformed
        DHistory dhistory = new DHistory(this, true);        
        String command = dhistory.getSelectedCommand();
        if (command != null){
         jTextAreaTerminal.setText(command);
        }
    }//GEN-LAST:event_jButtonHistoryActionPerformed
 
    private void jButtonExecActionPerformed(java.awt.event.ActionEvent evt) 
    {//GEN-FIRST:event_jButtonExecActionPerformed
                   exec();
    }//GEN-LAST:event_jButtonExecActionPerformed
 
    private void jTextAreaTerminalKeyPressed(java.awt.event.KeyEvent evt) 
    {//GEN-FIRST:event_jTextAreaTerminalKeyPressed
        if (evt.getKeyCode() == KeyEvent.VK_F5){
         exec();
        }
    }//GEN-LAST:event_jTextAreaTerminalKeyPressed
 
    private void exec(){
         String command = jTextAreaTerminal.getText().trim();
         if (command == null || command.equals("")){
           JOptionPane.showMessageDialog(this, "O comando não pode ser vazio");
         }else{
         Map<String,String> result = TerminalEmulator.exec(command);
         
         if (result.get("error") != null && !result.get("error").equals("")){
                  writeOutput(result.get("error"), true);
         }else if (result.get("input") != null && !result.get("input").equals("")){
                  writeOutput(result.get("input"), false);
         }
         }
    }
    
    private void clearOutput(){
         jTextAreaOutput.setText("");
    }
    
    
    private void paintOutput(boolean error){
     if (error){
          jTextAreaOutput.setForeground(Color.RED);
     }else{
          jTextAreaOutput.setForeground(Color.BLACK);
     }
    }
    
    private void writeOutput(String text, boolean error){
         clearOutput();
         paintOutput(error);
         jTextAreaOutput.setText(text);      
    }
    
    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {
        /* Set the Nimbus look and feel */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
         */
        try {
            for (javax.swing.UIManager.LookAndFeelInfo info : 
            javax.swing.UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException ex) {
            java.util.logging.Logger.getLogger(FTerminal.class.getName()).log(
            java.util.logging.Level.SEVERE, null, ex);
        } catch (InstantiationException ex) {
            java.util.logging.Logger.getLogger(FTerminal.class.getName()).log(
            java.util.logging.Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            java.util.logging.Logger.getLogger(FTerminal.class.getName()).log(
            java.util.logging.Level.SEVERE, null, ex);
        } catch (javax.swing.UnsupportedLookAndFeelException ex) {
            java.util.logging.Logger.getLogger(FTerminal.class.getName()).log(
            java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>
 
        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new FTerminal().setVisible(true);
            }
        });
    }
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton jButtonExec;
    private javax.swing.JButton jButtonHistory;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JScrollPane jScrollPane2;
    private javax.swing.JTextArea jTextAreaOutput;
    private javax.swing.JTextArea jTextAreaTerminal;
    // End of variables declaration//GEN-END:variables
}
Listagem 2. Fterminal.java

Se você estiver usando Netbeans para criação de formulários perceberá que alguns métodos são padrões e não perderemos muito tempo neles:

  • public Fterminal(): Construtor padrão do formulário;
  • initComponents(): Inicializa os componentes do formulário;
  • main(): Método que possibilita a execução do formulário como sendo o formulário principal do projeto.

Vejamos agora os métodos importantes a serem explicados:

  • clearOutput(): Apenas limpa o conteúdo que de saída do jTextAreaOutput, que pode ser um ERRO retornado ou um conteúdo válido;
  • paintOutptu(boolean error): Pinta o jTextAreaOutput de acordo com a condição error, sendo que, ao ocorrer um erro a mensagem será pintada de VERMELHO, caso contrário PRETA;
  • writeOutput(String text, boolean error): Ao executar um comando no TerminalEmulator a saída deste comando é escrita no jTextAreaOutput através deste método;
  • exec(): Captura o texto digitado no jTextAreaTerminal e chama a sua execução para o TerminalEmulator. Caso a chave “error” não tenha nenhum conteúdo, então ele chama o writeOutput indicando que não há erros e passa o valor contido na chave “input”.
  • jTextAreaTerminalKeyPressed: Verifica se foi pressionada a tecla F5 e chama o método exec(), possibilitando sua execução rápida;
  • jButtonExecActionPerformed: Chama diretamente o método exec() quando o botão “Executar” for clicado;
  • jButtonHistoryActionPerformed: Este método está ligado ao botão “Histórico” e é executado no seu click. A sua função é chamar a classe Dhistory (que será mostrada mais a frente) e capturar o comando que foi selecionado neste formulário. Caso algum comando tenha sido selecionado, então este é setado no jTextAreaTerminal, possibilitando que o usuário possa re-executá-lo.

Dhistory

O objetivo desta classe, que trata-se de um Jdialog, é mostrar o histórico de comandos já executados, possibilitar a limpeza destes ou a sua execução, conforme a Figura 2.

Dhistory
Figura 2. Dhistory

O formulário Dhistory possui um Jlist que contém a lista de comandos já executados, além de um botão Confirmar, que seleciona um comando e o envia para o Fterminal afim de ser executado. O botão “Limpar” faz chamada ao clearHistory() do TerminalEmulator afim de limpar o histórico de comandos executados.

Vejamos a classe Dhistory.java por completo na Listagem 3.


package br.com.lab;

import javax.swing.DefaultListModel;

public class DHistory extends javax.swing.JDialog {

  /**
      *
      */
     private static final long serialVersionUID = 1L;
     private DefaultListModel lista = new DefaultListModel();
  
  /**
   * Creates new form DHistory
   */
  public DHistory(javax.swing.JFrame parent, boolean modal) {
      super(parent, modal);
      initComponents();
      loadHistory();
      setVisible(true);
  }
  
  private void loadHistory(){
       jListHistory.setModel(lista);
       for(String hi : TerminalEmulator.getCommandHistory()){
        lista.addElement(hi);
       }
  }

  /**
   * This method is called from within the constructor to initialize the form.
   * WARNING: Do NOT modify this code. The content of this method is always
   * regenerated by the Form Editor.
   */
  @SuppressWarnings("unchecked")
  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
  private void initComponents() {

      jScrollPane1 = new javax.swing.JScrollPane();
      jListHistory = new javax.swing.JList();
      jButtonConfirm = new javax.swing.JButton();
      jButtonClear = new javax.swing.JButton();

      setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);

      jScrollPane1.setViewportView(jListHistory);

      jButtonConfirm.setText("Confirmar");
      jButtonConfirm.addActionListener(new java.awt.event.ActionListener() {
          public void actionPerformed(java.awt.event.ActionEvent evt) {
              jButtonConfirmActionPerformed(evt);
          }
      });

      jButtonClear.setText("Limpar");
      jButtonClear.addActionListener(new java.awt.event.ActionListener() {
          public void actionPerformed(java.awt.event.ActionEvent evt) {
              jButtonClearActionPerformed(evt);
          }
      });

      javax.swing.GroupLayout layout = new javax.swing.
      GroupLayout(getContentPane());
      getContentPane().setLayout(layout);
      layout.setHorizontalGroup(
          layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
          .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, 
          layout.createSequentialGroup()
              .addContainerGap()
              .addComponent(jScrollPane1, javax.swing.GroupLayout.
              DEFAULT_SIZE, 376, Short.MAX_VALUE)
              .addContainerGap())
          .addGroup(layout.createSequentialGroup()
              .addGap(107, 107, 107)
              .addComponent(jButtonConfirm)
              .addGap(18, 18, 18)
              .addComponent(jButtonClear, javax.swing.GroupLayout.
              PREFERRED_SIZE, 79, javax.swing.GroupLayout.PREFERRED_SIZE)
              .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
      );
      layout.setVerticalGroup(
          layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
          .addGroup(layout.createSequentialGroup()
              .addContainerGap()
              .addComponent(jScrollPane1, javax.swing.GroupLayout.
              PREFERRED_SIZE, 230, javax.swing.GroupLayout.PREFERRED_SIZE)
              .addGap(18, 18, 18)
              .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                  .addComponent(jButtonConfirm)
                  .addComponent(jButtonClear))
              .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
      );

      pack();
  }// </editor-fold>//GEN-END:initComponents

  private void jButtonConfirmActionPerformed(java.awt.event.ActionEvent evt)
  {//GEN-FIRST:event_jButtonConfirmActionPerformed
      dispose();
  }//GEN-LAST:event_jButtonConfirmActionPerformed

  private void jButtonClearActionPerformed(java.awt.event.ActionEvent evt) 
  //GEN-FIRST:event_jButtonClearActionPerformed
      TerminalEmulator.clearHistory();
      jListHistory.removeAll();
      dispose();
  }//GEN-LAST:event_jButtonClearActionPerformed

  public String getSelectedCommand(){
     if (jListHistory.getSelectedValue() != null){
           return jListHistory.getSelectedValue().toString();
     }else{
           return null;
     }
  }
  
  /**
   * @param args the command line arguments
   */
  public static void main(String args[]) {
      /* Set the Nimbus look and feel */
      //<editor-fold defaultstate="collapsed" desc=" Look and feel 
      setting code (optional) ">
      /* If Nimbus (introduced in Java SE 6) is not available, stay with 
      the default look and feel.
       * For details see http://download.oracle.com/javase/tutorial/uiswing/
       lookandfeel/plaf.html
       */
      try {
          for (javax.swing.UIManager.LookAndFeelInfo info : 
          javax.swing.UIManager.getInstalledLookAndFeels()) {
              if ("Nimbus".equals(info.getName())) {
                  javax.swing.UIManager.setLookAndFeel(info.getClassName());
                  break;
              }
          }
      } catch (ClassNotFoundException ex) {
          java.util.logging.Logger.getLogger(DHistory.class.getName())
          .log(java.util.logging.Level.SEVERE, null, ex);
      } catch (InstantiationException ex) {
          java.util.logging.Logger.getLogger(DHistory.class.getName())
          .log(java.util.logging.Level.SEVERE, null, ex);
      } catch (IllegalAccessException ex) {
          java.util.logging.Logger.getLogger(DHistory.class.getName())
          .log(java.util.logging.Level.SEVERE, null, ex);
      } catch (javax.swing.UnsupportedLookAndFeelException ex) {
          java.util.logging.Logger.getLogger(DHistory.class.getName())
          .log(java.util.logging.Level.SEVERE, null, ex);
      }
      //</editor-fold>

      /* Create and display the dialog */
      java.awt.EventQueue.invokeLater(new Runnable() {
          public void run() {
              DHistory dialog = new DHistory(new javax.swing.JFrame(), true);
              dialog.addWindowListener(new java.awt.event.WindowAdapter() {
                  @Override
                  public void windowClosing(java.awt.event.WindowEvent e) {
                      System.exit(0);
                  }
              });
              dialog.setVisible(true);
          }
      });
  }
  // Variables declaration - do not modify//GEN-BEGIN:variables
  private javax.swing.JButton jButtonClear;
  private javax.swing.JButton jButtonConfirm;
  private javax.swing.JList jListHistory;
  private javax.swing.JScrollPane jScrollPane1;
  // End of variables declaration//GEN-END:variables
}
Listagem 3. Dhistory.java

Logo no início temos a definição de um DefaultListModel que será usado no Jlist, afim de realizar as operações de inserção e remoção de elementos deste componente. Logo após, no construtor da classe, temos o uso de um método chamado loadHistory() que fará o carregamento de todos os históricos no componente jListHistory e a configuração do DefaultListModel:


private void loadHistory(){
  jListHistory.setModel(lista);
  for(String hi : TerminalEmulator.getCommandHistory()){
  lista.addElement(hi);
 }
}

Perceba que é feita uma iteração nos comandos do TerminalEmulator para adicionar um por um no DefaultListModel.

O botão “Confirmar”, que está ligado ao método jButtonConfirmActionPerformed, só tem a função de fechar o formulário, pois o que nos interessa é apenas selecionar um comando para posteriormente capturá-lo de outros formulários. Fazemos isso através do método getSelectedCommand() que retorna o comando selecionado caso ele exista.

Por fim, temos o botão “Limpar” que está ligado ao método jButtonClearActionPerformed(), que por usa vez faz chamada ao método clearHistory() do TerminalEmulator limpando o histórico e os valores do JlistHistory e, finalmente faz o dispose() do formulário, afinal não há porque o usuário continuar naquele formulário se não há mais comandos a serem selecionados.

Lembre-se que após selecionar o comando e clicar em Confirmar o formulário é fechado, voltando a execução do programa para o Fterminal que irá, por sua vez, usar o getSelectedCommand() para inserir o valor selecionado no jTextAreaTerminal.

Vimos neste artigo como criar um emulador de terminal Linux em Java, usando a classe Runtime e diversos outros recursos do Java. É importante salientar que melhorias podem ser realizadas neste projeto, pois trata-se apenas de um protótipo para fins didáticos. Você pode ir muito além, adicionando o histórico de comandos ao banco de dados, hora de execução, quem executou, valor retornado e etc.

Aprenda ainda mais