Shell Script e Banco de Dados

Thobias Salazar Trevisan
Primeira versão: 28 de Julho de 2003


  1. Introdução
  2. Preparando o Terreno
  3. O Básico
  4. Colocando dados na tabela
  5. Pesquisando Dados
  6. Removendo Registro
  7. Atualizando um Registro
  8. PostgreSQL
  9. Considerações Finais
  10. Agradecimentos


1. Introdução

Muitas pessoas, quando precisam fazer aplicações "mais complexas" e que utilizem banco de dados, optam por uma linguagem de programação diferente de Shell Script. Muitas delas não compreendem o poder de um script em shell e/ou desconhecem a linguagem interpretada contida nele.

Este tutorial se propõe a mostrar como podemos integrar shell script com banco de dados. Nos exemplos usaremos o MySQL, devido ao fato dele ser GPL, fácil e rápido. Mas a idéia central abordada neste texto pode ser aplicada também a outros bancos de dados. No final, mostrarei como podemos utilizar a mesma idéia com PostgreSQL. Será mostrado o básico, apenas para iniciar o leitor no assunto.

Ao final, você verá o poder de um shell script interagindo com um banco de dados, enxergará a luz e conhecerá a doutrina dos amantes do shell. =8)

1.1. Pré-requisitos

Como não vou abordar o básico sobre shell script e SQL, espera-se que você já tenha conhecimento de ambos.

1.2. Sobre o Tutorial

Na primeira parte, veremos os passos básicos para configurar o MySQL. Falaremos rapidamente sobre como "ajeitar" ele para os nossos scripts, criando uma base de dados e a tabela sobre as quais realizaremos os experimentos. Na segunda parte, cobriremos todos os comandos básicos SQL (INSERT, SELECT, DELETE e UPDATE) com vários exemplos, pois este é um guia prático. Veremos códigos, códigos, códigos... =8) Por último, alguns exemplos usando PostgreSQL.


2. Preparando o Terreno

Vamos ao que interessa. Veremos os passos essenciais para criarmos um banco de dados para interagir com os nossos scripts. É recomendado que você leia alguns tutoriais sobre MySQL antes de colocar o seu sistema em produção.

Neste ponto espera-se que você já tenha o mysql-server e o mysql-client do MySQL instalados. Como os nomes dos pacotes do server e do client variam entre as distribuições Linux, verifique se você tem os arquivos mysqld e mysql.

  prompt> which mysqld
  /usr/sbin/mysqld
  prompt> 
  prompt> which mysql
  /usr/bin/mysql
  prompt>

Tudo certo, vamos iniciar o servidor MySQL:

  prompt> /etc/init.d/mysql start

Algumas distribuições inicializam o daemon do MySQL sem permitir acesso remoto. No Debian, por exemplo, caso você queira permitir, comente a seguinte linha no arquivo /etc/mysql/my.cnf:

  #skip-networking

Se você alterou o arquivo, não se esqueça de reiniciar o daemon.

  prompt> /etc/init.d/mysql restart 

Na instalação padrão, o MySQL cria um usuário sem senha chamado root que tem controle total sobre ele. Assim, a primeira coisa que faremos será definir uma senha para este usuário:

  prompt> mysql -u root mysql
  Reading table information for completion of table and column names
  You can turn off this feature to get a quicker startup with -A
  
  Welcome to the MySQL monitor.  Commands end with ; or \g.
  Your MySQL connection id is 3 to server version: 3.23.49-log
  
  Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
  
  mysql>
  mysql>
  mysql> SET PASSWORD FOR 'root'@'localhost' = PASSWORD('senha');
  Query OK, 0 rows affected (0.00 sec)
  
  mysql>

Troque "senha" para a senha que você escolher. Pronto, definimos uma senha para o usuário root. Agora vamos criar uma base de dados para os nossos experimentos e adicionar um usuário para utilizá-la.

  prompt> mysql -u root mysql -p
  Enter password:
  Reading table information for completion of table and column names
  You can turn off this feature to get a quicker startup with -A
  
  Welcome to the MySQL monitor.  Commands end with ; or \g.
  Your MySQL connection id is 4 to server version: 3.23.49-log
  
  Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
  
  mysql>
  mysql> create database mysql_bash;
  Query OK, 1 row affected (0.00 sec)
  
  mysql> GRANT usage ON *.* TO thobias@localhost IDENTIFIED BY 'senha';
  Query OK, 0 rows affected (0.00 sec)
  
  mysql> GRANT usage ON *.* TO thobias@'%' IDENTIFIED BY 'senha';
  Query OK, 0 rows affected (0.00 sec)
  
  mysql> GRANT CREATE, DROP, SELECT, INSERT, UPDATE, DELETE ON mysql_bash.* TO thobias@localhost;
  Query OK, 0 rows affected (0.00 sec)
  
  mysql> GRANT CREATE, DROP, SELECT, INSERT, UPDATE, DELETE ON mysql_bash.* TO thobias@'%';
  Query OK, 0 rows affected (0.00 sec)
  
  mysql>

O primeiro comando (create database mysql_bash;), cria uma base de dados com o nome mysql_bash, ou seja, um diretório onde ficarão as nossas tabelas.

O segundo comando cria um usuário chamado thobias com a senha senha, e dá permissão para ele se conectar ao banco de dados. O terceiro comando difere do segundo pelo fato de dar permissão para o usuário thobias se conectar ao banco à partir de qualquer máquina. Poderíamos especificar a máquina, exemplo:

  mysql> GRANT usage ON *.* TO thobias@maquina1.com IDENTIFIED BY 'senha';

O % é um curinga que especifica qualquer máquina. Caso você não execute o terceiro comando, você poderá acessar o banco somente na mesma máquina que está rodando o banco, em outras palavras, o localhost.

Os dois últimos comandos servem para dar permissão ao usuário thobias fazer o que quiser na base da dados mysql_bash.

Note: o usuário só pode mexer na base de dados mysql_bash. Nas demais ele não tem permissão

Feito isto, execute:

  mysql> flush privileges;
  Query OK, 0 rows affected (0.00 sec)
  
  mysql> quit
  Bye
  prompt>

Beleza, já estamos com o nosso banco de dados pronto para começar a brincadeira. =8)


3. O Básico

Shell script, diferente de PHP, Perl, C, Python e outras linguagens de programação, não tem interação direta com o MySQL. Toda comunicação entre os scripts com o banco de dados se dá através do cliente, ou seja, um comando.

O shell para se comunicar com o banco de dados usa o cliente (comando mysql)

O grande esquema está no fato do cliente do MySQL ser muito flexível, aceitando comandos SQL da entrada padrão (stdin), através da linha de comando (utilizando a opção -e), fazendo dump da base, etc.

Como típico exemplo de banco de dados, vamos criar uma agenda. :P Primeiro, criaremos uma tabela chamada agenda:

  prompt> mysql -u thobias -psenha -e "CREATE TABLE agenda \ 
  (nome VARCHAR(50) NOT NULL PRIMARY KEY,telefone VARCHAR(150), \
  email VARCHAR(35), aniversario DATE)" mysql_bash

A sintaxe utilizada no comando acima foi:

  prompt> mysql -u user -psenha -e "cmd SQL" base de dados

-u especifica o usuário para se conectar ao banco de dados
-p senha do usuário. (note que não existe espaço entre o -p e a senha
-e o comando SQL que queremos executar
mysql_bash a base de dados sobre a qual será realizado o comando SQL

Como sabemos que o comando foi executado com sucesso ou não ???

Simples. Como todo comando UNIX ele retorna 0 se foi executado com sucesso ou um número diferente de 0 caso ocorra algum erro.

Assim, após executarmos aquele comando testamos o $?:

  prompt> echo $?
  0
  prompt>

Caso nós executássemos de novo o comando para criar a tabela agenda, ocorreria um erro, pois já existe uma tabela com este nome na base de dados:

  prompt> mysql -u thobias -psenha -e "CREATE TABLE agenda \
  (nome VARCHAR(50) NOT NULL PRIMARY KEY,telefone VARCHAR(150),\ 
  email VARCHAR(35), aniversario DATE)" mysql_bash
  
  ERROR 1050 at line 1: Table 'agenda' already exists
  prompt> 
  prompt> echo $?
  1
  prompt>

O echo $? retornou um número diferente de 0. Assim, podemos colocar o comando em um if, ou usar os operadores lógicos && e ||, ou seja, tratá-lo como um típico comando UNIX.

Então vamos começar a utilizar o comando acima e apenas trocar o comando SQL para mexer nos nossos dados. Mas aí vem um "problema". Se quando executamos aquele comando, algum usuário executa um ps aux. hmmmmm nossa senha vai aparecer, e aí já viu né :/

A partir da versão 3.22, por padrão o cliente e o servidor na inicialização lêem o arquivo de configuração ~/.my.cnf, para pegarem opções específicas dos usuários. Podemos criar este arquivo com as seguintes linhas:

  prompt> cat .my.cnf << FIM
  > [client]
  > password=senha
  > FIM
  [client]
  password=senha
  prompt> 
  prompt> 
  prompt> cat .my.cnf 
  [client]
  password=senha
  prompt>

Dizemos que o cliente deve usar aquela senha.

Agora não precisamos mais passar a senha na linha de comando.

  prompt> mysql -u thobias -e "show tables" mysql_bash
  +----------------------+
  | Tables_in_mysql_bash |
  +----------------------+
  | agenda               |
  +----------------------+
  prompt>

Caso você queira executar o comando SQL em uma máquina remota, utilize a opção -h e o host. Exemplo:

  prompt> mysql -h maquina -u thobias -e "show tables" mysql_bash

Para mais informações consulte o MySQL Reference Manual.


4. Colocando dados na tabela

Bom, já temos uma base de dados (mysql_bash), já criamos uma tabela (agenda). Para lembrar os campos da tabela:

  prompt> mysql -u thobias -e "desc agenda" mysql_bash
  +-------------+--------------+------+-----+---------+-------+
  | Field       | Type         | Null | Key | Default | Extra |
  +-------------+--------------+------+-----+---------+-------+
  | nome        | varchar(50)  |      | PRI |         |       |
  | telefone    | varchar(150) | YES  |     | NULL    |       |
  | email       | varchar(35)  | YES  |     | NULL    |       |
  | aniversario | date         | YES  |     | NULL    |       |
  +-------------+--------------+------+-----+---------+-------+
  prompt>

Existem várias maneiras de se inserir dados na tabela. Vamos ver algumas:

4.1. Arquivo com os Dados

Vamos supor que temos um arquivo com os dados, onde existe um registro por linha e os campos estão separados pelo caractere dois-pontos :.

1º campo nome
2º campo telefone
3º campo E-mail
4º campo data de aniversário

  prompt> cat dados_entrada.txt 
  Super Man:(21)1234-5678 trabalho:superman@palaciodajustica.com:1965-10-25
  Batman::batman@batmail.com:1970-05-15
  Robin:(21) 9999-1111:robin@batmail.com:1985-12-25
  Mulher Maravilha::M.maravilha@bizarro.com:
  prompt>

Note que alguns campos não têm valor.

4.1.1. Inserindo cada registro por vez

Uma maneira seria criar um script, onde pegamos linha a linha e criamos um SQL para inserir os dados:

  prompt> cat input_1.sh
  #!/bin/bash
  #
  # $1 = arquivo com os dados já formatados
  #
  
  IFS=:
  
  while read nome fone mail aniver;do
  	echo
  	echo "Nome:        $nome"
  	echo "Telefone:    $fone"
  	echo "mail:        $mail"
  	echo "Aniversário: $aniver"
  
  	mysql -u thobias -e \
  "INSERT INTO agenda VALUES('$nome','$fone','$mail','$aniver')" mysql_bash
  
  	[ "$?" = "0" ] && echo "Operacao OK" || echo "Operação: ERRO"
  done < $1
  
  prompt>

executando:

  prompt> ./input_1.sh dados_entrada.txt 
  
  Nome:        Super Man
  Telefone:    (21)1234-5678 trabalho
  mail:        superman@palaciodajustica.com
  Aniversário: 1965-10-25
  Operacao OK
  
  Nome:        Batman
  Telefone:    
  mail:        batman@batmail.com
  Aniversário: 1970-05-15
  Operacao OK
  
  Nome:        Robin
  Telefone:    (21) 9999-1111
  mail:        robin@batmail.com
  Aniversário: 1985-12-25
  Operacao OK
  
  Nome:        Mulher Maravilha
  Telefone:    
  mail:        M.maravilha@bizarro.com
  Aniversário: 
  Operacao OK
  prompt> 

Mas imagine que temos um arquivo imenso. Fazer uma conexão com o banco para executar cada insert não é a melhor alternativa.

4.1.2. Criando um Arquivo SQL

Podemos criar um arquivo com todos os INSERTs SQL separados por ponto-e-vírgula ;, e passar este arquivo para o comando mysql através da entrada padrão (stdin).

  prompt> cat input_2.sh 
  #!/bin/bash
  #
  # $1 = arquivo com os dados já formatados
  #
  # Será gerado um arquivo 'arquivo.SQL' com os comandos SQL
  #
  
  IFS=:
  
  while read nome fone mail aniver;do
  	echo "INSERT INTO agenda VALUES('$nome','$fone','$mail','$aniver');" >> arquivo.SQL
  done < $1
  
  prompt>

Note que estamos gerando um arquivo (arquivo.SQL) com os comandos SQL.

  prompt> ./input_2.sh dados_entrada.txt 

Vamos ver nossa saída:

  prompt> cat arquivo.SQL 
  INSERT INTO agenda VALUES('Super Man','(21)1234-5678 trabalho','superman@palaciodajustica.com','1965-10-25');
  INSERT INTO agenda VALUES('Batman','','batman@batmail.com','1970-05-15');
  INSERT INTO agenda VALUES('Robin','(21) 9999-1111','robin@batmail.com','1985-12-25');
  INSERT INTO agenda VALUES('Mulher Maravilha','','M.maravilha@bizarro.com','');
  prompt>

Agora, para incluir os novos dados, executamos:

  prompt> mysql -u thobias mysql_bash < arquivo.SQL
  ERROR 1062 at line 1: Duplicate entry 'Super Man' for key 1
  prompt> 

ou

  prompt> cat arquivo.SQL | mysql -u thobias mysql_bash 
  ERROR 1062 at line 1: Duplicate entry 'Super Man' for key 1
  prompt>

Ops! o nosso primeiro insert já existe. No nosso caso, todos os insert já existem, mas imagine que podem haver alguns que ainda não existam e não sabemos quais são. O cliente mysql, por default, vai parar de executar os comandos SQL assim que algum comando retornar um erro. Para garantirmos que ele executará todos os comandos, mesmo que alguns retornem erros, utilizamos a opção -f.

  prompt> mysql -u thobias -f mysql_bash < arquivo.SQL 
  ERROR 1062 at line 1: Duplicate entry 'Super Man' for key 1
  ERROR 1062 at line 2: Duplicate entry 'Batman' for key 1
  ERROR 1062 at line 3: Duplicate entry 'Robin' for key 1
  ERROR 1062 at line 4: Duplicate entry 'Mulher Maravilha' for key 1
  prompt>

Note que, esta tática de colocar vários comandos SQL em um arquivo e passá-lo para o cliente mysql, pode ser utilizado com qualquer comando SQL dentro do arquivo utilizando o ponto-e-vírgula para separá-los. Por exemplo, na instalação de um programa você pode criar um arquivo.SQL com os comandos para criar as tabelas necessárias, popular o banco com alguns dados iniciais, etc.

4.2. Interativo

Outra possibilidade é fazer um script para ler do teclado uma nova entrada para a base:

  prompt> cat input_3.sh
  #!/bin/bash
  #
  # Repare que a data para o MySQL é aaaa-mm-dd
  #
  
  # Testa se a data recebida, no formato do MySQL, é válida ou não
  checa_data(){
  [ $(echo "$1" | sed 's,[12][0-9]\{3\}/\(0[1-9]\|1[012]\)/\(0[1-9]\|[12][0-9]\|3[01]\),,') ] &&
  return 1 || return 0
  }
  
  echo "Entre com os dados para incluir na agenda"
  echo
  read -p "Nome       : " nome
  read -p "Telefone   : " fone
  read -p "E-Mail     : " mail
  read -n2 -p "Aniversário (dia/mes/ano): " dia
  read -n2 -p "/" mes
  read -n4 -p "/" ano
  echo
  
  #colocamos na variável aniver a data no formato do MySQL
  aniver="$ano/$mes/$dia"
  echo
  
  # se a data não for nula
  if [ "$ano" -o "$mes" -o "$dia" ];then
  	# testa se a data é válida
  	checa_data "$aniver" || { echo "ERRO: Data de aniversário inválida";exit; }
  fi
  
  # não aceitamos nomes nulo
  [ "$nome" ] || { echo "ERRO: nome inválido";exit; }
  
  read -p "Deseja Incluir (s/n)? "
  if [ "$REPLY" = "s" ];then
  	mysql -u thobias -e\
  	"INSERT INTO agenda VALUES('$nome','$fone','$mail','$aniver')" mysql_bash
  
  	[ "$?" = "0" ] && echo "Operacao OK" || echo "Operação: ERRO"
  fi
  
  prompt>

Executando:

  prompt> ./input_3.sh
  Entre com os dados para incluir na agenda
  
  Nome       : Formiga Atomica
  Telefone   : 
  E-Mail     : formiga@formigueiro.com
  Aniversário (dia/mes/ano): 12/12/1930
  
  Deseja Incluir (s/n)? s
  Operacao OK
  prompt>


5. Pesquisando Dados

Para pesquisar dados na nossa base de dados utilizamos o comando SQL SELECT. Vamos fazer um script que procure por um nome na tabela e mostra todas informações. Utilizaremos o curinga * para diferenciar a pesquisa exata da parcial (usando o LIKE do SQL).

  prompt> cat search.sh 
  #!/bin/bash
  #
  # $1 = nome a procurar
  #
  # exemplo de uso:
  # $ ./search.sh Super
  # $ ./search.sh Super*
  # $ ./search.sh *Super*
  #
  # O * é um curinga, fazendo procura parcial (usando o LIKE)
  #
  
  # Testa pra ver se é procura exata ou parcial
  if [ "$*" = "${*#*\*}" ];then
  	# Faz pesquisa exata do nome
  	mysql -u thobias -e\
  	"SELECT * FROM agenda WHERE nome = '$*'" mysql_bash
  
  # Procura por partes do nome
  else
  	# ${*//\\*/%} = troca todos * por %, que é o curinga do LIKE
  	mysql -u thobias -e\
  	"SELECT * FROM agenda WHERE nome LIKE '${*//\\*/%}'" mysql_bash
  fi
  
  prompt>

Para testar vamos procurar pelo Super Man. :)

  prompt> ./search.sh super
  prompt> 
  prompt> ./search.sh super*
  +-----------+------------------------+-------------------------------+-------------+
  | nome      | telefone               | email                         | aniversario |
  +-----------+------------------------+-------------------------------+-------------+
  | Super Man | (21)1234-5678 trabalho | superman@palaciodajustica.com | 1965-10-25  |
  +-----------+------------------------+-------------------------------+-------------+
  prompt>
  prompt> ./search.sh super man
  +-----------+------------------------+-------------------------------+-------------+
  | nome      | telefone               | email                         | aniversario |
  +-----------+------------------------+-------------------------------+-------------+
  | Super Man | (21)1234-5678 trabalho | superman@palaciodajustica.com | 1965-10-25  |
  +-----------+------------------------+-------------------------------+-------------+
  prompt>

Note que a primeira pesquisa não retornou nada, pois não existe um registro onde o campo nome tenha o valor igual a super. Quando usamos o curinga *, dizemos qualquer campo que começe por super.

Vamos procurar por batman:

  prompt> ./search.sh bat
  prompt>
  prompt> ./search.sh batman
  +--------+----------+--------------------+-------------+
  | nome   | telefone | email              | aniversario |
  +--------+----------+--------------------+-------------+
  | Batman |          | batman@batmail.com | 1970-05-15  |
  +--------+----------+--------------------+-------------+
  prompt>

A segunda pesquisa obteve sucesso, pois casou um registro com o campo nome igual ao que nós procurávamos.

Note que o curinga funciona para um lado específico:

  prompt> ./search.sh *bat
  prompt> 
  prompt> ./search.sh bat*
  +--------+----------+--------------------+-------------+
  | nome   | telefone | email              | aniversario |
  +--------+----------+--------------------+-------------+
  | Batman |          | batman@batmail.com | 1970-05-15  |
  +--------+----------+--------------------+-------------+
  prompt> 
  prompt> ./search.sh *atm*
  +--------+----------+--------------------+-------------+
  | nome   | telefone | email              | aniversario |
  +--------+----------+--------------------+-------------+
  | Batman |          | batman@batmail.com | 1970-05-15  |
  +--------+----------+--------------------+-------------+
  prompt>

Se quisermos usar o curinga para procurar por qualquer dado, temos que colocá-lo entre aspas para o shell não interpretá-lo:

  prompt> ./search.sh *
  prompt> 
  prompt> ./search.sh "*"
  +------------------+------------------------+-------------------------------+-------------+
  | nome             | telefone               | email                         | aniversario |
  +------------------+------------------------+-------------------------------+-------------+
  | Robin            | (21) 9999-1111         | robin@batmail.com             | 1985-12-25  |
  | Batman           |                        | batman@batmail.com            | 1970-05-15  |
  | Super Man        | (21)1234-5678 trabalho | superman@palaciodajustica.com | 1965-10-25  |
  | Mulher Maravilha |                        | M.maravilha@bizarro.com       | 0000-00-00  |
  | Formiga Atomica  |                        | formiga@formigueiro.com       | 1930-12-12  |
  +------------------+------------------------+-------------------------------+-------------+
  prompt>

Vamos deixar a saída mais fru-fru. O MySQL utiliza como separador de campos o TAB (\t), então colocaremos o \t como o separador de campos default (IFS):

  prompt> cat search2.sh 
  #!/bin/bash
  #
  # $1 = nome a procurar
  #
  # exemplo de uso:
  # $ ./search2.sh Super
  # $ ./search2.sh Super*
  # $ ./search2.sh *Super*
  #
  # O * é um curinga, fazendo procura parcial
  #
  
  # Testa pra ver se é procura exata ou parcial
  if [ "$*" = "${*#*\*}" ];then 
  	# procura exata
  	S=$(mysql -u thobias -e \
  	"SELECT * FROM agenda WHERE nome = '$*'" mysql_bash)
  # Procura por partes do nome
  else
  	# ${*//\\*/%} = troca todos * por %, que é o curinga do LIKE
  	S=$(mysql -u thobias -e \
  	"SELECT * FROM agenda WHERE nome LIKE'${*//\\*/%}'" mysql_bash)
  fi
  
  # a procura retornou algum registro ?!
  [ "$S" ] || { echo "Registro não encontrado";exit; }
  
  # colocar um TAB como IFS
  IFS="$(echo -e '\t')"
  # Apagamos a primeira linha, pois ela contém o nome dos campos
  S=$(echo "$S" | sed '1d')
  
  echo "$S" | while read nome fone mail aniver;do
  	echo "
  Nome       : $nome
  Telefone   : $fone
  E-mail     : $mail
  Aniversário: $aniver"
  
  done 
  
  prompt>

Fazendo uma consulta:

  prompt> ./search2.sh batman
  
  Nome       : Batman
  Telefone   : batman@batmail.com
  E-mail     : 1970-05-15
  Aniversário: 
  prompt>

OPS! Algo está errado. O E-mail está com a data de aniversário?! Se analisarmos o registro do batman veremos que ele não tem telefone:

  prompt> ./search.sh batman
  +--------+----------+--------------------+-------------+
  | nome   | telefone | email              | aniversario |
  +--------+----------+--------------------+-------------+
  | Batman |          | batman@batmail.com | 1970-05-15  |
  +--------+----------+--------------------+-------------+
  prompt>

Como o separador de registros é o TAB e nós cadastramos o telefone dele usando aspas simples '' no INSERT do SQL, ele não mostra o NULL, ficando então, dois TABs grudados (\t\t). Nós utilizamos o IFS com TAB. Para simular o que está acontecendo vamos a um exemplo simples:

  prompt> IFS="$(echo -en '\t')"
  prompt> echo -e "1\t\t3\t4" | while read a b c d;do echo "a=$a b=$b c=$c d=$d";done
  a=1 b=3 c=4 d=
  prompt>

Repare que no echo não tem o valor 2, sendo omitido por dois TABs seguidos. Só que o nosso read não está considerando isto, ele está tratando TABTAB como 1 campo. Para resolver vamos usar um sed macho para colocar um espaço em branco entre os TABs

  prompt> echo -e "1\t\t3\t4" | sed ":a;s/\(`echo -e '\t'`\)\(\1\)/\1 \2/;ta" |\
  while read a b c d;do echo "a=$a b=$b c=$c d=$d";done
  a=1 b=  c=3 d=4

Segundo problema é a data, na saída do MySQL está aaaa-mm-dd, vamos fazer um outro sed para tratar a data:

  prompt> echo 1970-05-15 | sed 's,\([0-9]\{4\}\)-\([0-9][0-9]\)-\([0-9][0-9]\),\3/\2/\1,'
  15/05/1970
  prompt>

Agora alteramos o script de consulta para utilizar estas táticas:

  prompt> cat consulta.sh
  #!/bin/bash
  #
  # $* = nome a procurar
  #
  # exemplo de uso:
  # $ ./consulta.sh Super
  # $ ./consulta.sh Super*
  # $ ./consulta.sh *Super*
  # $ ./consulta.sh super man
  #
  # O * é um curinga, fazendo procura parcial
  #
  
  # converte data de aaaa-mm-dd para dia/mes/ano
  data_mysql-to-brasil(){
  echo "$*" | sed 's,\([0-9]\{4\}\)-\([0-9][0-9]\)-\([0-9][0-9]\),\3/\2/\1,'
  }
  
  # Testa pra ver se é procura exata ou parcial
  if [ "$*" = "${*#*\*}" ];then 
  	# procura exata
  	S=$(mysql -u thobias -e \
  	"SELECT * FROM agenda WHERE nome = '$*'" mysql_bash)
  # Procura por partes do nome
  else
  	# ${*//\\*/%} = troca todos * por %, que é o curinga do LIKE
  	S=$(mysql -u thobias -e \
  	"SELECT * FROM agenda WHERE nome LIKE '${*//\\*/%}'" mysql_bash)
  fi
  
  # a procura retornou algum registro ?!
  [ "$S" ] || { echo "Registro não encontrado";exit; }
  
  # colocar um TAB como IFS
  IFS="$(echo -e '\t')"
  # Apagamos a primeira linha, pois ela contém o nome dos campos
  S=$(echo "$S" | sed '1d')
  
  # colocamos um espaço em branco entre TABs repetidos (\t\t)
  echo "$S" | sed ":a;s/\(`echo -e '\t'`\)\(\1\)/\1 \2/;ta" | \
  while read nome fone mail aniver; do
  
  	echo "
  Nome       : $nome
  Telefone   : $fone
  E-mail     : $mail"
  echo "Aniversário: $(data_mysql-to-brasil $aniver)"
  done
  
  prompt>

Para testar, algumas consultas:

  prompt> consulta.sh batman
  
  Nome       : Batman
  Telefone   :  
  E-mail     : batman@batmail.com
  Aniversário: 15/05/1970
  prompt> 
  prompt> consulta.sh super*
  
  Nome       : Super Man
  Telefone   : (21)1234-5678 trabalho
  E-mail     : superman@palaciodajustica.com
  Aniversário: 25/10/1965
  prompt> 
  prompt> 
  prompt> consulta.sh "*"
  
  Nome       : Robin
  Telefone   : (21) 9999-1111
  E-mail     : robin@batmail.com
  Aniversário: 25/12/1985
  
  Nome       : Batman
  Telefone   :  
  E-mail     : batman@batmail.com
  Aniversário: 15/05/1970
  
  Nome       : Super Man
  Telefone   : (21)1234-5678 trabalho
  E-mail     : superman@palaciodajustica.com
  Aniversário: 25/10/1965
  
  Nome       : Mulher Maravilha
  Telefone   :  
  E-mail     : M.maravilha@bizarro.com
  Aniversário: 00/00/0000
  
  Nome       : Formiga Atomica
  Telefone   :  
  E-mail     : formiga@formigueiro.com
  Aniversário: 12/12/1930
  prompt> 


6. Removendo Registro

Agora as coisas mudam um pouco. No DELETE, não importa se o registro existe ou não, o código de retorno sempre será zero, ou seja, echo $? é 0.

  prompt> mysql -u thobias -e "DELETE FROM agenda WHERE nome = 'nenhum'" mysql_bash
  prompt> echo $?
  0
  prompt>

Mas repare que não existe ninguém com o nome nenhum na base. Para resolver o problema teremos que utilizar a opção -vv, que retorna algumas informações e dentre elas, quantas "linhas" da tabela foram alteradas.

  prompt> mysql -vv -u thobias -e "DELETE FROM agenda WHERE nome = 'nenhum'" mysql_bash
  --------------
  DELETE FROM agenda WHERE nome = 'nenhum'
  --------------
  
  Query OK, 0 rows affected (0.00 sec)
  
  Bye
  prompt>

Repare 0 rows. Agora com um sed pra filtrar a saída:

  prompt> mysql -vv -u thobias -e "DELETE FROM agenda WHERE nome = 'nenhum'" mysql_bash |\
  sed -n '/^Query/s/.*, \([0-9]\+\) rows.*/\1/p'
  0
  prompt>

Pronto, agora podemos tratar o "código de retorno" =8)

O script abaixo deleta um registro procurando pelo nome.

  prompt> cat deleta.sh
  #!/bin/bash
  #
  # $* = nome a procurar
  #
  # exemplo de uso:
  # $ ./deleta.sh Super
  # $ ./deleta.sh Super*
  # $ ./deleta.sh *Super*
  # $ ./deleta.sh super man
  #
  # O * é um curinga, fazendo procura parcial
  #
  
  # Retorna quantas linhas a query SQL alterou na base de dados
  linha(){
  	sed -n '/^Query/s/.*, \([0-9]\+\) row.*/\1/p'
  }
  
  # Testa pra ver se é procura exata ou parcial
  if [ "$*" = "${*#*\*}" ];then # procura exata
  	S=$(mysql -vv -u thobias -e \
  	"DELETE FROM agenda WHERE nome = '$*'" mysql_bash | linha)
  	
  	[ $S -eq 0 ] && echo "Registro não encontrado" || echo "Foram deletado(s) $S registro(s)"
  
  # Procura por partes do nome
  else
  	S=$(mysql -vv -u thobias -e \
  	"DELETE FROM agenda WHERE nome LIKE '${*//\\*/%}'" mysql_bash | linha)
  
  	[ $S -eq 0 ] && echo "Registro não encontrado" || echo "Foram deletado(s) $S registro(s)"
  fi
  
  prompt>

Testando:

  prompt> ./deleta.sh nenhum
  Registro não encontrado
  prompt>
  prompt> 
  prompt> ./deleta.sh formi*
  Foram deletado(s) 1 registro(s)
  prompt> 
  prompt> ./deleta.sh formi*
  Registro não encontrado
  prompt>


7. Atualizando um Registro

Como no delete, aqui ocorre o mesmo problema. Pois o UPDATE do SQL sempre retorna zero. Bom, sempre retorna zero se a query SQL estiver correta, caso contrário, o $? terá um valor diferente de zero.

  prompt> mysql -u thobias -e "DELETE FROM agenda WHERE campo_invalido = 'nada'" mysql_bash
  ERROR 1054 at line 1: Unknown column 'campo_invalido' in 'where clause'
  prompt> 
  prompt> echo $?
  1
  prompt>

Repare que a query SQL está procurando o nada no campo campo_invalido da tabela agenda. Como este campo não existe, o código de retorno foi 1.

Para descobrirmos se foi alterado algum registro na base temos que utilizar a opção -vv, para ver quantas linhas (rows) o comando SQL alterou.

  prompt> mysql -vv -u thobias -e "UPDATE agenda SET telefone='9999999' WHERE nome ='robin'" mysql_bash
  --------------
  UPDATE agenda SET telefone='9999999' WHERE nome ='robin'
  --------------
  
  Query OK, 0 rows affected (0.00 sec)
  Rows matched: 1  Changed: 0  Warnings: 0
  
  Bye
  prompt>

Podemos utilizar o mesmo algoritmo e o mesmo sed do DELETE. Só que aqui precisamos saber qual campo o usuário gostaria de atualizar.

O script abaixo recebe como entrada o campo da tabela a ser atualizado e o nome da pessoal que será feita a atualização.

  prompt> cat atualiza.sh 
  #!/bin/bash
  #
  # $* = nome a procurar
  #
  # exemplo de uso:
  # $ ./atualiza.sh telefone super man
  # $ ./atualiza.sh nome super*
  # $ ./atualiza.sh aniversario *super*
  #
  # O * é um curinga, fazendo procura parcial
  #
  
  # Testa se a data recebida, no formato do MySQL, é válida ou não
  checa_data(){
  [ $(echo "$1" | sed 's,[12][0-9]\{3\}/\(0[1-9]\|1[012]\)/\(0[1-9]\|[12][0-9]\|3[01]\),,') ] &&
  return 1 || return 0
  }
  
  # Retorna quantas linhas a query SQL alterou na base de dados
  linha(){
  	sed -n '/^Query/s/.*, \([0-9]\+\) row.*/\1/p'
  }
  
  # testa se o campo a alterar é válido
  [ "$1" != nome -a "$1" != telefone -a "$1" != email -a "$1" != aniversario ] &&
  { echo "Campos Validos: nome telefone email aniversario";exit; }
  
  # colocamos em campo o $1,e um shift para podermos usar o $*
  campo=$1
  shift
  
  # se é aniversário é data então para o MySQL é aaaa-mm-dd
  if [ "$campo" = "aniversario" ];then
  	read -n2 -p "Novos dados para Aniversário (dia/mes/ano): " dia
  	read -n2 -p "/" mes
  	read -n4 -p "/" ano
  	dados="$ano/$mes/$dia"
  	echo
  
  	# testa se a data é válida
  	checa_data "$dados" || { echo "ERRO: Data de aniversário inválida";exit; }
  else
  	read -p "Novos dados para $campo: " dados
  	# se é para trocar o nome, não aceitamos o nome nulo
  	[ "$campo" = "nome" -a ! "$dados" ] && { echo "ERRO: nome inválido";exit; }
  fi
  
  # Testa pra ver se é procura exata ou parcial
  if [ "$*" = "${*#*\*}" ];then # procura exata
  	S=$(mysql -vv -u thobias -e \
  	"UPDATE agenda SET $campo='$dados' WHERE nome = '$*'" mysql_bash | linha)
  	
  	[ $S -eq 0 ] && echo "Registro não encontrado" || echo "Foram alterado(s) $S registro(s)"
  # Procura por partes do nome
  else
  	S=$(mysql -vv -u thobias -e \
  	"UPDATE agenda SET $campo='$dados' WHERE nome LIKE '${*//\\*/%}'" mysql_bash | linha)
  
  	[ $S -eq 0 ] && echo "Registro não encontrado" || echo "Foram alterado(s) $S registro(s)"
  fi
  
  prompt>

Hora de testar:

  prompt> atualiza.sh
  Campos Validos: nome telefone email aniversario
  prompt>
  prompt> atualiza.sh formiga
  Campos Validos: nome telefone email aniversario
  prompt>
  prompt> atualiza.sh telefone robin
  Novos dados para telefone: 111112222 casa
  Foram alterado(s) 1 registro(s)
  prompt>
  prompt> atualiza.sh telefone supe*
  Novos dados para telefone: 99999999
  Foram alterado(s) 1 registro(s)
  prompt> 
  prompt> atualiza.sh telefone nenhum
  Novos dados para telefone: 12122
  Registro não encontrado
  prompt>


8. PostgreSQL

Outro banco de dados muito utilizado e GPL é o PostgreSQL. Como tinha comentado, o shell não se comunica diretamente com o banco de dados, ou seja, nós precisamos do cliente. No PostgreSQL, o nome do programa cliente é psql. Como no MySQL, ele tem várias opções que nos permitem trabalhar com shell. Por exemplo, ele aceita, através da opção -h, realizar um comando SQL em uma máquina remota, especificar o usuário com a opção -U, gerar saída HTML com o uso da opção -H e mais um monte de coisas. Mas a parte principal é que ele também aceita comandos SQL via linha de comando e gera o resultado na saída padrão (stdout). Exemplo:

  prompt> su postgres
  prompt>
  prompt> createdb mysql_bash
  prompt>
  prompt> psql mysql_bash
  Welcome to psql, the PostgreSQL interactive terminal.
  
  Type:  \copyright for distribution terms
         \h for help with SQL commands
         \? for help on internal slash commands
         \g or terminate with semicolon to execute query
         \q to quit
  
  mysql_bash=# 
  mysql_bash=# CREATE TABLE agenda (nome VARCHAR(50),email VARCHAR(35));
  CREATE
  mysql_bash=# \q
  prompt>

Inserindo alguns dados via linha de comando:

  prompt> echo "INSERT INTO agenda VALUES ('Super Man','superman@palaciodajustica.com')" | psql -f- mysql_bash
  INSERT 16572 1
  prompt>
  prompt> echo "INSERT INTO agenda VALUES ('Batman','batman@batmail.com')" | psql -f- mysql_bash 
  INSERT 16573 1
  prompt>
  prompt> echo "\d agenda" | psql -U postgres -f- mysql_bash 
                 Table "agenda"
   Column |         Type          | Modifiers 
  --------+-----------------------+-----------
   nome   | character varying(50) | 
   email  | character varying(35) | 
  
  prompt>
  prompt> echo "\pset border 2; \d agenda" | psql -U postgres -f- mysql_bash 
  Border style is 2.
                  Table "agenda"
  +--------+-----------------------+-----------+
  | Column |         Type          | Modifiers |
  +--------+-----------------------+-----------+
  | nome   | character varying(50) |           |
  | email  | character varying(35) |           |
  +--------+-----------------------+-----------+
  
  prompt>

Podemos também usar a opção -c.

  prompt> psql -U postgres -c "SELECT * FROM agenda" mysql_bash
     nome    |             email             
  -----------+-------------------------------
   Super Man | superman@palaciodajustica.com
   Batman    | batman@batmail.com
  (2 rows)
  
  prompt>

Com a opção -f podemos especificar um arquivo contendo os comandos SQL ou usar um hífen para ele ler os comandos da entrada padrão (stdin). Com a opção -c, ele executa uma query SQL.

Para mais detalhes: man psql

Viu, também é simples integrar shell script com PostgreSQL.


9. Considerações Finais

Fizemos um tour pelo maravilhoso mundo do shell script com banco de dados. Analisamos todas as operações básicas do SQL, ou seja, o básico você já sabe. O que mudará serão os comandos SQL que, "no mundo real", serão mais complexos ou maiores. Mas o tratamento para eles serão os mesmos descritos neste tutorial.

Vários outros scripts deveriam ser feitos e os scripts acima deveriam ser estendidos para termos uma verdadeira agenda. Por exemplo, nosso script de pesquisa só aceita pesquisas pelo campo nome, então se você quer saber de quem é o telefone xxxxxx, não dá. Mas, para adicionarmos esta funcionalidade, o algoritmo do script será o mesmo, alterando somente o comando SQL para fazer o SELECT pelo campo telefone.

Estas extensões vou deixar para você fazer e, assim, se divertir um pouco também. Pô, só eu que faço a melhor parte! =8)

Imagine o poder de um shell script interagindo com um banco de dados mais CGI, dialog, etc.

10. Agradecimentos


This HTML page is (see source)