Um script para compilar com o GnuCobol

Photo by Alex Knight on Pexels

Por que automatizar com um script?

Para gerar um programa executável no 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 desenvolvimento 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 se trabalhamos com subprogramas e user defined functions. Um script nesse caso facilita nosso dia a dia e nos desobriga de lembrar 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, e vai funcionar tanto no MacOS quanto no Windows com WSL, desde que o bash esteja disponível.

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 caminho e 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 e CPYB guardam os nomes dos diretórios onde estão os copybooks locais (que ainda estão na máquina do usuário) e os 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 serão passadas para o 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 modificados mais tarde quando o fonte for analisado. As demais variáveis, ESQL e FUNC 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 tarde, deverá ser compilado 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 (único parágrafo obrigatório da IDENTIFICATION DIVISION) possui sete espaços à esquerda. Se esse for o caso o programa provavelmente 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

O script verifica então 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 de comentário, o que levaria a decidir por opções de compilação equivocadas. Mas do jeito que está, já me ajudou bastante.