Um script para compilar com o GnuCobol

Photo by Alex Knight on Pexels

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.