NAME

ufpp(1f) - [DEVELOPER] a Fortran source code pre-processor (LICENSE:PD)

SYNOPSIS

   ufpp  [[ -D] define_list]
         [ -I include_directories]
         [ -i input_file(s)]
         [ -o output_file]
         [ -html]
         [ -system]
         [ -q
         [ -header text_for_first_line]
         [ -ident]
         [ -prefix character_ADE]
         [ -keeptabs]
         [ -noenv]
         [ -width n]
         [ -cstyle default|doxygen]
         [ -allow_links]
         [ -verbose]
         [ -debug]
         [ -d ignore|remove|blank]
         [ -version]
         [ -help [ -html]]

DESCRIPTION

The ufpp(1) command is a basic file pre-processor in the style of fpp(1) and cpp(1) that allows simple embedded directives to be used to conditionally select source code lines.

Additional features allow ufpp(1) to easily support single files that contain documentation, source code, and unit testing materials and commands.

A detailed description of command options can be generated by using the "-help" switch , which was used to generate ufpp.1.html.

ufpp(1) is relatively language-agnostic, but is particularly designed with Fortran in mind. It is in fact written in Fortran. You will therefore find ufpp(1) particularly useful if you desire a Fortran-based pre-processor that is simple enough that you can easily customize it.

More specifically source file lines can be selected conditionally using basic directives ($IF, $ELSE, $ELSEIF, $ENDIF, $DEFINE, $UNDEFINE, ...) The syntax for numeric expressions in the directives is very similar to FORTRAN 77 INTEGER and LOGICAL expressions.

Additionally ufpp(1) supports

EXAMPLES

a basic input file ...

Assuming you are familiar with the basic behavior of preprocessors such as cpp(1), fpp(1), coco(1), or more powerful macro processors such as m4(1) let us start with a basic input file example:


 $! Compile this program and one of the following versions of subroutine SUB1 depending on the value of A
 $! NOTE: This Fortran source file contains ufpp(1) directives beginning with a dollar("$") character.
 $!------------------
 $! set default values for variables tested
 $IF .NOT.DEFINED(A)
 $  DEFINE A=1
 $ENDIF
 $!------------------
    program conditional_compile
       call sub1
    end program conditional_compile
 $IF A .EQ. 1        ! If A=1 output this version of subroutine sub1
    subroutine sub1
       print*, "This is the first SUB1"
    end subroutine sub1
 $ELSEIF A .EQ. 2    ! If A=2 output this version of subroutine sub1
    subroutine sub1
       print*, "This is the second SUB1"
    end subroutine sub1
 $ELSE               ! If A was not 1 or 2 output this version of subroutine sub1
    subroutine sub1
       print*, "This is the third SUB1"
    end subroutine sub1
 $ENDIF

Assuming this example is in the file "basic.ff" the output of the command


   ufpp a=2 -i basic.ff -o basic.f90

would be


   program conditional_compile
      call sub1
   end program conditional_compile
   subroutine sub1
      print*, "This is the second SUB1"
   end subroutine sub1

The next example shows how regions of flat text can be processed by ufpp(1): o The $BLOCK HELP directive can be used to produce a routine to display help text and the help text can also be written to a file to produce further document formats. o The $BLOCK VERSION directive can be used a to produce a routine that displays a version in a way that is compatible with the metadata display program what(1). o The $BLOCK NULL directive can be used for text that is otherwise ignored. o The $BLOCK SHELL directive can be used for text that is the output of a shell. This can be very system-dependent but allows code to be built dynamically. o The VARIABLE option is used to rewrite the text as a string declaration

$BLOCK NULL -file notes.txt !@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ This section uses $BLOCK NULL to let a block of text be included in the source that is essentially ignored. The difference between this and a $IFDEF .FALSE. ... $ENDIF or $OUTPUT /dev/null .. $OUTPUT END section is that the -file switch lets this section get written to a file optionally when the $UFPP_DOCUMENT_DIR variable is set. This text could be markdown text, HTML, RTF, LaTex or some other format to be post-processed independently. The following $BLOCK HELP section directives is converted into the HELP_USAGE() subroutine by ufpp. Optionally if the environment variable $UFPP_DOCUMENT_DIR is set the text is additionally written as-is into $UFPP_DOCUMENT_DIR/doc/cf.1.man because the optional "-file cf.1.man" switch was specified. This additional file would then typically be run thru txt2man(1) to create a man(1) page. $BLOCK END !@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ $IFDEF F90 $BLOCK HELP -file cf.1.man NAME cf - Convert between Fahrenheit and Celsius temperature values SYNOPSIS cf [ -C val1 val2 val3 ...] [ -F val1 val2 val3 ...]|[ --help| --version] DESCRIPTION -C [ val(1) val(2) val(3) ... ] Display the given Celsius values as both Celsius and Fahrenheit values -F [ val(1) val(2) val(3) ... ] Display the given Fahrenheit values as both Celsius and Fahrenheit values If no values are given a small table of common temperatures is displayed --help display this help and exit --version output version information and exit At the physically impossible-to-reach temperature of zero Kelvin, or minus 459.67 degrees Fahrenheit (minus 273.15 Celsius), atoms would stop moving. As such, nothing can be colder than absolute zero on the Kelvin scale. $BLOCK END $!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ $! this text is converted into the help_version subroutine by ufpp ... $! in a format that works with what(1), do not use ",>,\ characters in the labels $BLOCK VERSION PRODUCT: GPF (General Purpose Fortran) utilities and examples PROGRAM: cf(1f) DESCRIPTI0N: convert multiple values between Celsius and Fahrenheit VERSION: 1.0, 2016-04-09 AUTHOR: John S. Urban REPORTING BUGS: http://www.urbanjost.altervista.org/ HOME PAGE: http://www.urbanjost.altervista.org/index.html LICENSE: Public Domain. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. $BLOCK END $!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ $! use output of a bash(1) shell here document to build some comments $BLOCK SHELL cat <<EOF !! date ....... $(date) !! userid ..... $(logname) !! hostname ... $(hostname) EOF $BLOCK END $!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ $BLOCK COMMENT This block of text becomes Fortran comment lines by having exclamations placed in front of each line, but is otherwise left as-is. The next section is just as-is Fortran $BLOCK END $!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ program cf use M_kracken, only: kracken, rgets, lget implicit none $IDENT cf(1f): convert multiple values between Celsius and Fahrenheit real,allocatable :: val(:) integer :: i, isum=0 call kracken('cf','-F -C -help .F. -version .F.' ) ! define and crack command line arguments call help_usage(lget('cf_help')) ! display help information and stop if true call help_version(lget('cf_version')) ! display version information and stop if true isum=0 ! running tally of values found on -C and -F options val=rgets('cf_C') ! get any values specified on -C option if(size(val).gt.0)then ! have something to print in C ==> F table isum=isum+size(val) write(*,'(a,t14,a)')'Celsius','Fahrenheit' write(*,'(f8.2,"C",t14,f8.2,"F")')& & ( val(i),(val(i)+40.0)*9.0/5.0 - 40.0,i=1,size(val)) ! print the requested values endif val=rgets('cf_F') ! check for values on -F if(size(val).gt.0)then isum=isum+size(val) write(*,'(a,t14,a)') 'Fahrenheit', 'Celsius' write(*,'(f8.2,"F",t14,f8.2,"C")')(val(i),(val(i)+40.0)*5.0/9.0 - 40.0,i=1,size(val)) endif if(isum.eq.0)then ! if no values given on -C and -F switches show default table val=[ & &-459.67, & & -20.0, -15.0, -10.0, -5.0, 0.0, & & 5.0, 10.0, 15.0, 20.0, 25.0, & & 30.0, 32.0, 35.0, 40.0, 45.0, & & 50.0, 55.0, 60.0, 65.0, 70.0, & & 75.0, 80.0, 85.0, 90.0, 95.0, & & 98.6, 100.0, 105.0, 110.0, 115.0 ] write(*,'(a,t14,a)') 'Fahrenheit', 'Celsius' write(*,'(f8.2,"F",t14,f8.2,"C")')(val(i),(val(i)+40.0)*5.0/9.0 - 40.0,i=1,size(val)) endif end program cf and the output file $ENDIF $!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ $IFDEF UFPP_TEST Normally you would not just put text here, but $SYSTEM and $OUTPUT sections to unit test your commands. Then you would call ufpp UFPP_TEST -system ... to run your tests, and ufpp F90 ... to generate the expanded source code when compiling $ENDIF $!@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
The output from "ufpp F90 -i FILE -system" is:
subroutine help_usage(l_help) implicit none character(len=*),parameter :: ident="@ (#)help_usage(3f): prints help information" logical,intent(in) :: l_help character(len=:),allocatable :: help_text(:) integer :: i if(l_help)then help_text=[ CHARACTER(LEN=128) :: & 'NAME ',& ' cf - Convert between Fahrenheit and Celsius temperature values ',& ' ',& 'SYNOPSIS ',& ' cf [-C val1 val2 val3 ...] [-F val1 val2 val3 ...][--help][--version] ',& ' ',& 'DESCRIPTION ',& ' -C [ val(1) val(2) val(3) ... ] ',& ' Display the given Celsius values as both Celsius and Fahrenheit values ',& ' -F [ val(1) val(2) val(3) ... ] ',& ' Display the given Fahrenheit values as both Celsius and Fahrenheit values ',& ' ',& ' If no values are given a small table of common temperatures is displayed ',& ' ',& ' --help display this help and exit ',& ' ',& ' --version ',& ' output version information and exit ',& ' ',& ' At the physically impossible-to-reach temperature of zero Kelvin, or ',& ' minus 459.67 degrees Fahrenheit (minus 273.15 Celsius), atoms would ',& ' stop moving. As such, nothing can be colder than absolute zero on the ',& ' Kelvin scale. ',& ' ',& ''] WRITE(*,'(a)')(trim(help_text(i)),i=1,size(help_text)) stop ! if -help was specified, stop endif end subroutine help_usage !----------------------------------------------------------------------------------------------------------------------------------- subroutine help_version(l_version) implicit none character(len=*),parameter :: ident="@(#)help_version(3f): prints version information" logical,intent(in) :: l_version character(len=:),allocatable :: help_text(:) integer :: i if(l_version)then help_text=[ CHARACTER(LEN=128) :: & '@(#)PRODUCT: GPF (General Purpose Fortran) utilities and examples>',& '@(#)PROGRAM: cf(1f)>',& '@(#)DESCRIPTION: convert multiple values between Celsius and Fahrenheit>',& '@(#)VERSION: 1.0, 2016-04-09>',& '@(#)AUTHOR: John S. Urban>',& '@(#)REPORTING BUGS: http://www.urbanjost.altervista.org/>',& '@(#)HOME PAGE: http://www.urbanjost.altervista.org/index.html>',& '@(#)LICENSE: Public Domain. This is free software: you are free>',& '@(#) to change and redistribute it. There is NO WARRANTY,>',& '@(#) to the extent permitted by law.>',& '@(#)COMPILED: Mon, Dec 18th, 2017 2:55:24 AM>',& ''] WRITE(*,'(a)')(trim(help_text(i)(5:len_trim(help_text(i))-1),kind=kind(1)),i=1,size(help_text)) stop ! if -version was specified, stop endif end subroutine help_version !----------------------------------------------------------------------------------------------------------------------------------- !! date ....... Mon, Dec 18, 2017 2:55:25 AM !! userid ..... JSU !! hostname ... buzz ! This block of text becomes Fortran comment lines by having exclamations ! placed in front of each line, but is otherwise left as-is. ! ! The next section is just as-is Fortran !=================================================================================================================================== program cf use M_kracken, only: kracken, rgets, lget implicit none character(len=*),parameter::ident="@ (#)cf(1f): convert multiple values between Celsius and Fahrenheit" real,allocatable :: val(:) integer :: i, isum=0 call kracken('cf','-F -C -help .F. -version .F.' ) ! define and crack command line arguments call help_usage(lget('cf_help')) ! display help information and stop if true call help_version(lget('cf_version')) ! display version information and stop if true isum=0 ! running tally of values found on -C and -F options val=rgets('cf_C') ! get any values specified on -C option if(size(val).gt.0)then ! have something to print in C ==> F table isum=isum+size(val) write(*,'(a,t14,a)')'Celsius','Fahrenheit' write(*,'(f8.2,"C",t14,f8.2,"F")')& & ( val(i),(val(i)+40.0)*9.0/5.0 - 40.0,i=1,size(val)) ! print the requested values endif val=rgets('cf_F') ! check for values on -F if(size(val).gt.0)then isum=isum+size(val) write(*,'(a,t14,a)') 'Fahrenheit', 'Celsius' write(*,'(f8.2,"F",t14,f8.2,"C")')(val(i),(val(i)+40.0)*5.0/9.0 - 40.0,i=1,size(val)) endif if(isum.eq.0)then ! if no values given on -C and -F switches show default table val=[ & &-459.67, & & -20.0, -15.0, -10.0, -5.0, 0.0, & & 5.0, 10.0, 15.0, 20.0, 25.0, & & 30.0, 32.0, 35.0, 40.0, 45.0, & & 50.0, 55.0, 60.0, 65.0, 70.0, & & 75.0, 80.0, 85.0, 90.0, 95.0, & & 98.6, 100.0, 105.0, 110.0, 115.0 ] write(*,'(a,t14,a)') 'Fahrenheit', 'Celsius' write(*,'(f8.2,"F",t14,f8.2,"C")')(val(i),(val(i)+40.0)*5.0/9.0 - 40.0,i=1,size(val)) endif end program cf

This small example shows an easy way to maintain help and version options easily for programs. Note that the M_kracken module was used to provide the example with command line argument parsing; but that is not required.

A more complicated example follows where code is contained in an HTML document. When the ufpp(1) switch "-html" is used or the input filename ends in ".html" , all lines outside of the lines delimited with the XMP directive are ignored, allowing documentation and code to be contained in one file that can be viewed via a browser.


    <html>
    <head>
       <title> Simple HTML template for use with ufpp(1) </title>
       <link rel="stylesheet" href="http://www.w3.org/StyleSheets/Core/OldStyle" type="text/css" />
       <!-- Chocolate Midnight Modernist Oldstyle Steely Swiss Traditional Ultramarine -->
    </head>
    <body>
    <h1> This is an HTML document that can be used as input to ufpp(1)</h1>
    <p>
        If you are comfortable writing simple HTML documents, a simple
        feature of ufpp(1) allows you to easily maintain documentation,
        source, and links to external files all together. If you run the
        ufpp(1) Fortran Preprocessor on this file with the command
        <pre>
        ufpp -html -i THISFILE.html -o THISFILE.f90
        </pre>
        the Fortran source is extracted using very simple rules: If lines
        begin with &lt;xmp&gt; start writing out the lines; quit if
        &lt;/xmp&gt; is encountered.
    </p>
    <h2> TEST PROGRAM </h2>
    <!-- =============================================================== -->
    <xmp>
    $if .not.defined(WHICH_VERSION)
    $define WHICH_VERSION=1 ! Will only compile the first version of subroutine ONE
    $endif
          program testit
             write(*,*)'hello world'
             call one()
             call two()
          end program testit
    </xmp>
    <!-- =============================================================== -->
    <h2> ROUTINE ONE </h2>
    <p>
         There are three versions of subroutine ONE(). If WHICH_VERSION
         is not defined, the "third" version is written to output.
         If WHICH_VERSION = 1, version "first" is used.
         If WHICH_VERSION =2 , version "second" is used.
    </p>
    <xmp>
    $IF WHICH_VERSION .EQ. 1
         subroutine one()
            write(*,*)'called one, first version'
         end subroutine one
    $ELSEIF WHICH_VERSION .eq. 2
         subroutine one()
            write(*,*)'called one, second version'
         end subroutine one
    $ELSE
         subroutine one()
            write(*,*)'called one, third version'
         end subroutine one
    $ENDIF
    </xmp>
    <!-- =============================================================== -->
    <h2> ROUTINE TWO</h2>
    <xmp>
          subroutine two()
             write(*,*)'called two'
          end subroutine two
    </xmp>
    <!-- =============================================================== -->
    </body>

History:

This customized pre-processor is derived from the public-domain Lahey pre-processor. Parsing of expressions remains significantly unchanged from the Lahey version; but the rest is essentially a re-write. ufpp(1) compiles with all the Fortran 90+ compilers I have needed it for; including the freely available gfortran(1) compiler (version 4.5.3+).

Notes on Fortran source code pre-processing with ufpp(1)

In practice Fortran codes rarely need traditional pre-processing. In the past the most common reason for passing Fortran files through a pre-processor was when the code was calling C code or other languages where there was no standard calling interface defined. The ISO_C_BINDING standard now defines a C-Fortran interface; so there is now generally less need to pre-process Fortran source. The second-most common need for pre-processing was to easily use different Fortran extensions in different environments. Since the most common extensions are now standardized (accessing command line arguments, getting date/time information, passing a system command to the operating system, ...) the need for traditional pre-processing has diminished even more. On the other hand dealing with differences between compilers that only partially implement newer Fortran versions has brought back a new need for pre-processing; but that is hopefully a temporary issue.

That being said, pre-processing is occasionally still required. I strongly recommend that you isolate such code by putting the code sections that require pre-processing into small procedures that perform just the non-standard operations, and keep this in the smallest number of files possible.

If I try to avoid the need for traditional pre-processing of Fortran source code whenever possible why not just use cpp(1) instead of making ufpp(1)? Even if traditional pre-processing is not required I find the ufpp(1) filtering capabilities useful as a way to keep related text files (documentation, test files, test programs, test scripts, ...) together in an easily maintained form.

Notes on alternatives to ufpp(1)

If you just want traditional pre-processing capabilities and use a major Linux or Unix distribution you may find existing tools already meet your needs.

Some Fortran compilers provide their own "Fortran-safe" pre-processors. The compiler documentation often describes their features; which are usually a subset of the Unix utility cpp(1). They often intentionally do not provide macro expansion or C-style comments (block or in-line); but otherwise look very much like cpp(1). They are often called "fpp" if they can be called as a stand-alone utility. Sun has an open-source version on netlib of their flavor of fpp(1).

The cpp(1) program is designed for C/C++ and is not totally "Fortran- safe". It is, however, a de-facto standard for code pre-processing. Different versions often have switches to reduce the chances of generating unexpected Fortran code. The most common version of cpp(1) works with most Fortran using the following form:


   cpp -P -C -traditional MYFILE.F90
   

The Fortran 95 standard provided an optional standard pre-processor definition. Currently, it seems to rarely be provided with compilers. Dan Nagle has an open-source version with macro expansion called "coco".

The Lahey Fortran site has a pre-processor code in a public-domain repository at http://www.lahey.com/code.htm. It is written in Fortran 77. This is what ufpp(1) was originally derived from. Typically you need to create a small wrapper script to call it from make(1)/cmake(1)/...

Other pre-processors that are Fortran-friendly or relatively language-agnostic are

Using alternate Fortran sources and/or INCLUDE files

Simply put, instead of pre-processors you can conditionally compile different files.

If you have a directory for each programming environment with identically named procedures in them that are specific to a system you can often avoid pre-processing altogether. Isolate the system-dependent code into small procedures to minimize duplicate code. Usually, the files are of the same name but in different directories, one per platform. You can often additionally reduce the amount of duplicate code by judicious use of Fortran INCLUDE files. The most common problem with this method is making sure you keep any changes to the procedure parameters consistent across all the versions of the same routine.

"INCLUDE" method

A variation on the previous approach is to have different directories with the same filenames in them that are INCLUDE files. You compile for different environments with the -I switch commonly available on Fortran compilers. So if you had directories CRAY and HP that both had the same files in them you could build different versions by entering "f90 -ICRAY ..." or "f90 -IHP ...".

Using dead code

If the system-dependent code is all "standard" code that will compile on all platforms some people recommend placing all the code in a procedure and setting a variable with an INCLUDE to select the proper code (assuming the selection does not cause major performance overhead).

This will make some branches into "dead code"; which many compilers will remove while optimizing the code.


     subroutine system_dependent()
     character(len=10) :: system
     include "system.h"
     if(system.eq.'hp')then
       write(*,*)'do hp stuff'
     elseif(system.eq.'cray')then
       write(*,*)'do cray stuff'
     else
       write(*,*)'error: unknown system'
     endif
     end subroutine system_dependent

Assuming you have multiple "system.h" files that include a line like


     system='hp'
     
or

     system='cray'
     
You can build different versions using the -I parameter once again to point to the different INCLUDE files.

I personally do not use this "dead code" method, but have seen it used to good effect.

Run-time selection

Of course when all the code can be compiled on all platforms you can write the code without any preprocessing or use of alternate INCLUDE files and provide input at run-time to select the correct branch as well. The difference is that the compiler cannot optimize out the unused branches in that case.

Procedure pointers

When all the code is standard a good method can be to use pointers to procedures, as is often used to select between different graphics drivers or various mathematical "solvers".