Um script para compilar com o GnuCobol

Para quê?
O comando para gerar um programa executável diretório corrente a partir de um fonte que está no mesmo diretório, a linha de comando é tão simples quanto…
$cobc -x p0001
Porém, à medida em que aumenta a complexidade do ambiente de trabalho e do sistema com o qual estamos trabalhando, essa linha de comando pode ficar mais complexa e consequentemente propensa a erros. Se o programa p0001, do exemplo anterior, fosse escrito em free format, com comandos SQL, usando copybooks que estão em outro diretório e cujo executável precisasse ser gerado em um terceiro diretório, o comando ficaria assim:
cobc -x -free -locsql ~/cbl/p0001.cbl -I ~/cpy -o ~/bin/p0001
Pode ficar ainda mais complicado quando também trabalhamos com subprogramas, funções definidas pelo usuário etc. Um script nesse caso facilita nosso dia a dia e nos desobriga a lembrar de cada uma das muitas opções que o compilador oferece.
Neste artigo, vou te apresentar passo a passo um bash script que eu uso todo dia no Linux. Ele pode ser usado tanto no terminal quanto em IDEs como o VSCode.
Recebendo o nome do programa a compilar
Logo depois da shebang e dos comentários iniciais recebemos o nome do programa em uma variável chamada FONTE:
#!/bin/bash
#------------------------------------------------------------------------------#
# Nome: cobol.sh
# Objetivo: Script usado para compilacao COBOL no VSCODE
#
# Data: Janeiro/2021
# Descricao: Seleciona os argumentos correspondentes do GnuCOBOL (cobc) em
# funcao das caracteristicas do fonte cujo nome foi informado
# via argumento. Com isso apenas uma tarefa de compilacao pre-
# cisa ser configurada no VSCode
#
# Data: Marco/2025
# Descricao: Alterado para compilar funcoes intrinsecas, gerando o nome do
# objeto em uppercase sem caracteres especiais
#------------------------------------------------------------------------------#
FONTE=$1
Nesse caso, o nome do programa deve ser informado com o caminho e a extensão, para permitir que o script funcione para qualquer diretório de fontes do ambiente.
Exemplo:
cobol.sh ~/cbl/p0001.cbl
Logo em seguida, o script decompõe o nome recebido em outras variáveis que serão usadas mais adiante:
BASE=${FONTE%/cbl*} # Retira tudo depois de /cbl
PROG=${FONTE##*/} # Retira o nome do diretorio
PROG=${PROG%%.*} # Retira a extensão
EXTL="so" # Extensao de programas called
EXTP="cob" # Extensao do fonte gerado pelo pre-compilador SQL
CPYA=${COBCPY%:*} # Retira tudo depois dos dois pontos
CPYB=${COBCPY##*:} # Retira tudo antes dos dois pontos
Os comentários deixam claro para que serve cada variável, mas vale a pena destacar como elas se encaixam no ambiente que eu uso. Nesse ambiente, cada usuário é criado com uma árvore de diretórios parecida com a mostrada abaixo:
/home |__/usuario |____/bin |____/cbl |____/cpy |____/lib
Os executáveis (main) ficam no ~/bin, os fontes em ~/cbl, os copybooks em ~/cpy e os subprogramas (called) em ~/lib.
As variáveis criadas no início do script, portanto, guardam valores que serão usados mais adiante para montar o comando cobc. A variável BASE, por exemplo, guarda o caminho do programa fonte antes do “/cbl”, para que mais tarde se possa usar esse diretório base para gerar o executável em $BASE/bin. As variáveis CPYA guardam os nomes dos diretórios onde estão os copybooks no ambiente, para que o compilador localize tanto copybooks locais (que ainda estão na máquina do usuário) quanto copybooks globais (já em produção e compartilhados por todos os sistemas).
Investigando o programa que será compilado
Em seguida, o script seta quatro flags que serão usados para saber que opções devem ser usadas pelo comando cobc:
MAIN=true
FREE=true
ESQL=false
FUNC=false
O script assume que o programa a ser compilado é do tipo main (que pode ser chamado diretamente) e que o programa foi codificado em free format. Esses flags poderão ser baixados mais adiante quando o fonte for analisado. As demais variáveis, ESQL e FUNC, por default estão marcadas como false. Elas vão indicar se o programa possui comandos EXEC SQL e se são funções definidas por usuário. Na investigação do fonte, mais adiante, elas poderão ser alteradas para true.
O script então verifica se o fonte existe…
# Verifica se o programa fonte existe
if [ ! -f $FONTE ]; then
echo "cobol.sh (ERRO): O programa $FONTE nao existe";
exit 1;
fi
…e logo em seguida busca por strings dentro do fonte para determinar que tipo de programa ele é. O trecho abaixo verifica se existe a palavra USING na mesma linha da declaração da PROCEDURE. Isso indica que o programa é chamado por outros programas e por isso, mais adiante, deverá ser gerado com uma opção específica e ter seu executável armazenado no diretório ~/lib, para subprogramas. Se não encontrar nenhuma linha com a palavra PROCEDURE, o script é encerrado:
# Se o programa tem USING na PROCEDURE DIVISION e' "called", senao e' "main"
FOUND=$(grep -i "PROCEDURE" $FONTE | grep -i "USING")
if [ ! -z "$FOUND" ]; then
MAIN=false
else
FOUND=$(grep -i "PROCEDURE" $FONTE)
if [ -z "$FOUND" ]; then
echo "cobol.sh (ERRO): Programa nao tem procedure division";
exit 1;
fi
fi
Já no teste a seguir, o script verifica se a palavra PROGRAM-ID possui sete espaços à esquerda, o que indica que o programa foi codificado em fixed format. Caso contrário, o script irá considerar que ele foi codificado em free format. Além disso, esses IFs também verificam se o programa foi codificado com PROGRAM-ID ou com FUNCTION-ID.
# Se o PROGRAM-ID tiver sete espacos 'a esquerda e' "fixed", senao e' "free"
FOUND=$(grep -i "^[[:space:]]\{7\}PROGRAM-ID" $FONTE)
if [ ! -z "$FOUND" ]; then
FREE=false
else
FOUND=$(grep -i "^[[:space:]]\{7\}FUNCTION-ID" $FONTE)
if [ ! -z "$FOUND" ]; then
FREE=false
else
FOUND=$(grep -i "PROGRAM-ID" $FONTE)
if [ -z "$FOUND" ]; then
FOUND=$(grep -i "FUNCTION-ID" $FONTE)
if [ -z "$FOUND" ]; then
echo "cobol.sh (ERRO): Programa nao tem nem program-id nem function-id";
exit 1;
fi
fi
fi
fi
# Se tiver FUNCTION-ID e' user defined function
FOUND=$(grep -i "^[[:space:]]\{7\}FUNCTION-ID" $FONTE)
if [ ! -z "$FOUND" ]; then
FUNC=true
else
FUNC=false
fi
Na sequência, o script verifica se há algum comando EXEC SQL no programa. Nesse caso, o programa terá que ser pré-compilado antes de passar pela compilação:
# Se houver um comando EXEC SQL o programa precisa ser pre-compilado
FOUND=$(grep -i "EXEC SQL" $FONTE)
if [ ! -z "$FOUND" ]; then
ESQL=true
fi
Com todos os flags setados, começa um ninho de IFs para chamar o compilador com as opções adequadas a cada situação. No trecho abaixo, o programa será compilado como principal (main), tendo ou não comandos SQL e em fixed ou free format.
# Monta os parametros de compilacao em funcao do que encontrou nos fontes
echo "cobol.sh (INFO): MAIN=$MAIN FREE=$FREE ESQL=$ESQL FUNC=$FUNC"
if [[ $MAIN == true ]]; then
OBJETO=$BASE/bin/$PROG
PRECOMPILADO=$BASE/cbl/$PROG.$EXTP
if [[ $FREE == true ]]; then
if [[ $ESQL == true ]]; then
esqlOC -Q -I $CPYA -I $CPYB -o $PRECOMPILADO $FONTE
cobc -x -free -locsql $PRECOMPILADO -I $COBCPY -o $OBJETO
#rm $PROG.$EXTP
else
cobc -x -free -locsql $FONTE -I $COBCPY -o $OBJETO
fi
else
if [[ $ESQL == true ]]; then
esqlOC -Q -I $CPYA -I $CPYB -o $PRECOMPILADO $FONTE
cobc -x -locsql $PRECOMPILADO -I $COBCPY -o $OBJETO
#rm $PROG.$EXTP
else
cobc -x -locsql $FONTE -I $COBCPY -o $OBJETO
fi
fi
O ELSE do IF acima contém as opções correspondentes a um programa called, da mesma forma, tendo comandos SQL ou não, seja em fixed ou free format. Repare que nesse bloco o primeiro IF verifica se o programa é uma user defined function. Nesse caso, ele precisa ser compilado como um subprograma normal (será armazenado no diretório ~/lib com extensão .so) mas o nome do objeto, por exigência do GnuCobol, deve ser gerado em uppercase e sem traços.
else
if [[ $FUNC == true ]]; then
OBJETO=${PROG//-} # Elimina tracos
OBJETO=${OBJETO^^} # Converte para uppercase
OBJETO=$BASE/lib/$OBJETO.$EXTL
else
OBJETO=$BASE/lib/$PROG.$EXTL
fi
PRECOMPILADO=$BASE/cbl/$PROG.$EXTP
if [[ $FREE == true ]]; then
if [[ $ESQL == true ]]; then
esqlOC -Q -I $CPYA -I $CPYB -o $PRECOMPILADO $FONTE
cobc -free -locsql $PRECOMPILADO -I $COBCPY -o $OBJETO
#rm $PROG.$EXTP
else
cobc -free -locsql $FONTE -I $COBCPY -o $OBJETO
fi
else
if [[ $ESQL == true ]]; then
esqlOC -Q -I $CPYA -I $CPYB -o $PRECOMPILADO $FONTE
cobc -locsql $PRECOMPILADO -I $COBCPY -o $OBJETO
#rm $PROG.$EXTP
else
cobc -locsql $FONTE -I $COBCPY -o $OBJETO
fi
fi
fi
O script termina recuperando o return code do compilador para exibir uma mensagem final.
RC=$?
if [ $RC == 0 ]; then
echo "cobol.sh (INFO): O programa $PROG foi gerado em $OBJETO"
else
echo "cobol.sh (ERRO): O programa $PROG NAO foi compilado"
fi
exit $RC
Conclusão
E é isso! Chega de combinar as diversas opções para chegar ao resultado correto.
É claro que algumas inferências podem funcionar para alguns padrões e algumas instalações e não para outras. Esse script não verifica, por exemplo, se PROCEDURE DIVISION + USING está em uma linha comentada ou não, o que levá-lo a decidir por opções de compilação equivocadas. Mas do jeito que está, já me ajudou bastante.