
/**
 * @file expMake.c
 *
 *  This module implements Makefile construction functions.
 *
 * @addtogroup autogen
 * @{
 */
/*
 *  This file is part of AutoGen.
 *  AutoGen Copyright (C) 1992-2018 by Bruce Korb - all rights reserved
 *
 * AutoGen is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * AutoGen is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Figure out how to handle the line continuation.
 * If the line we just finished ends with a backslash, we're done.
 * Just add the newline character.  If it ends with a semi-colon or a
 * doubled amphersand or doubled or-bar, then escape the newline with a
 * backslash.  If the line ends with one of the keywords "then", "in" or
 * "else", also only add the escaped newline.  Otherwise, add a
 * semi-colon, backslash and newline.
 *
 * @param  ppzi  pointer to pointer to input text
 * @param  ppzo  pointer to pointer to output text
 * @param  tabch line prefix (tab) character
 * @param  bol   pointer to start of currently-being-output line
 *
 * @returns false to say the newline is dropped becase we're done
 *          true  to say the line was appended with the newline.
 */
static bool
handle_eol(char ** ppzi, char ** ppzo, char tabch, char * bol)
{
    char * pzScn = *ppzi;
    char * pzOut = *ppzo;
    int    l_len = (int)(pzOut - bol);

    /*
     *  Backup past trailing white space (other than newline).
     */
    while (IS_NON_NL_WHITE_CHAR(pzOut[-1]))
        pzOut--;

    /*
     *  Skip over empty lines, but leave leading white space
     *  on the next non-empty line.
     */
    {
        char * pz = pzScn;
        while (IS_WHITESPACE_CHAR(*pz)) {
            if (*(pz++) == NL)
                pzScn = pz;
        }
    }

    /*
     *  The final newline is dropped.
     */
    if (*pzScn == NUL)
        return false;

    switch (pzOut[-1]) {
    case '\\':
        /*
         *  The newline is already escaped, so don't
         *  insert our extra command termination.
         */
        *(pzOut++) = NL;
        break;

    case '&':
        /*
         *  A single ampersand is a backgrounded command.  We must terminate
         *  those statements, but not statements conjoined with '&&'.
         */
        if ('&' != pzOut[-2])
            goto append_statement_end;
        /* FALLTHROUGH */

    case '|':
    case ';':
    skip_semi_colon:
        /*
         *  Whatever the reason for a final '|', '&' or ';',
         *  we will not add a semi-colon after it.
         */
        memcpy(pzOut, MAKE_SCRIPT_NL + 2, MAKE_SCRIPT_NL_LEN - 2);
        pzOut += MAKE_SCRIPT_NL_LEN - 2;
        break;

    case 'n': // "then" or "in"
        if (l_len < 3)
            goto append_statement_end;

        if (pzOut[-2] == 'i') {
            if ((l_len == 3) || IS_WHITESPACE_CHAR(pzOut[-3]))
                goto skip_semi_colon;
            goto append_statement_end;
        }

        if (  (l_len < 5)
           || (  (l_len > 5)
              && ! IS_WHITESPACE_CHAR(pzOut[-5]) ))
            goto append_statement_end;
        if (strncmp(pzOut-4, HANDLE_EOL__THE, HANDLE_EOL__THE_LEN) == 0)
            goto skip_semi_colon;
        goto append_statement_end;

    case 'e': // "else"
        if (  (l_len < 5)
           || (  (l_len > 5)
              && ! IS_WHITESPACE_CHAR(pzOut[-5]) ))
            goto append_statement_end;
        if (strncmp(pzOut-4, HANDLE_EOL__ELS, HANDLE_EOL__ELS_LEN) == 0)
            goto skip_semi_colon;
        goto append_statement_end;

    default:
    append_statement_end:
        /*
         *  Terminate the current command and escape the newline.
         */
        memcpy(pzOut, MAKE_SCRIPT_NL, MAKE_SCRIPT_NL_LEN);
        pzOut += MAKE_SCRIPT_NL_LEN;
    }

    /*
     *  We have now started our next output line and there are still data.
     *  Indent with a tab, if called for.  If we do insert a tab, then skip
     *  leading tabs on the line.
     */
    if (tabch) {
        *(pzOut++) = tabch;
        while (*pzScn == tabch)  pzScn++;
    }

    *ppzi = pzScn;
    *ppzo = pzOut;
    return true;
}

/**
 * Pass through untreated sedable lines.  Sometimes it is just very useful
 * to post-process Makefile files with sed(1) to clean it up.
 *
 * @param txt   pointer to text.  We skip initial white space.
 * @param tab   pointer to where we stash the tab character to use
 * @returns     true to say this was a sed line and was emitted,
 *              false to say it was not and needs to be copied out.
 */
static bool
handle_sed_expr(char ** src_p, char ** out_p)
{
    char * src = *src_p;
    char * out = *out_p;

    switch (src[1]) {
    case 'i':
        if (strncmp(src+2, HANDLE_SED_IFNDEF, HANDLE_SED_IFNDEF_LEN) == 0)
            break;
        if (strncmp(src+2, HANDLE_SED_IFDEF, HANDLE_SED_IFDEF_LEN) == 0)
            break;
        return false;

    case 'e':
        if (strncmp(src+2, HANDLE_SED_ELSE, HANDLE_SED_ELSE_LEN) == 0)
            break;
        if (strncmp(src+2, HANDLE_SED_ENDIF, HANDLE_SED_ENDIF_LEN) == 0)
            break;
        /* FALLTHROUGH */
    default:
        return false;
    }

    {
        char * p = BRK_NEWLINE_CHARS(src);
        size_t l;
        if (*p == NL) /* do not skip NUL */
            p++;
        l = (size_t)(p - src);
        memcpy(out, src, l);
        *src_p = src + l;
        *out_p = out + l;
    }

    return true;
}

/**
 * Compute a maximal size for the output script.  Leading and trailing white
 * space are trimmed.  Dollar characters will likely be doubled and newlines
 * may get as many as MAKE_SCRIPT_NL_LEN characters inserted.  Make sure
 * there's space.
 *
 * @param txt   pointer to text.  We skip initial white space.
 * @param tab   pointer to where we stash the tab character to use
 * @returns     the maximum number of bytes required to store result.
 */
static size_t
script_size(char ** txt_p, char * tab)
{
    char * txt = *txt_p;
    char * ptxte;
    size_t sz  = 0;

    /*
     *  skip all blank lines and other initial white space
     *  in the source string.
     */
    if (! IS_WHITESPACE_CHAR(*txt))
        *tab = TAB;
    else {
        txt = SPN_WHITESPACE_CHARS(txt + 1);
        *tab  = (txt[-1] == TAB) ? NUL : TAB;
    }

    /*
     *  Do nothing with empty input.
     */
    if (*txt == NUL)
        return 0;

    /*
     *  "txt" is now our starting point.  Do not modify it any more.
     */
    *txt_p = txt;

    for (ptxte = txt - 1;;)  {
        ptxte = BRK_MAKE_SCRIPT_CHARS(ptxte+1);
        if (*ptxte == NUL)
            break;
        sz += (*ptxte == '$') ? 1 : MAKE_SCRIPT_NL_LEN;
    }

    ptxte = SPN_WHITESPACE_BACK(txt, ptxte);
    *ptxte = NUL;
    sz += (size_t)(ptxte - txt);
    return sz;
}

/*=gfunc makefile_script
 *
 * what:  create makefile script
 * general_use:
 *
 * exparg: text, the text of the script
 *
 * doc:
 *  This function will take ordinary shell script text and reformat it
 *  so that it will work properly inside of a makefile shell script.
 *  Not every shell construct can be supported; the intent is to have
 *  most ordinary scripts work without much, if any, alteration.
 *
 *  The following transformations are performed on the source text:
 *
 *  @enumerate
 *  @item
 *  Trailing whitespace on each line is stripped.
 *
 *  @item
 *  Except for the last line, the string, " ; \\" is appended to the end of
 *  every line that does not end with certain special characters or keywords.
 *  Note that this will mutilate multi-line quoted strings, but @command{make}
 *  renders it impossible to use multi-line constructs anyway.
 *
 *  @item
 *  If the line ends with a backslash, it is left alone.
 *
 *  @item
 *  If the line ends with a semi-colon, conjunction operator, pipe (vertical
 *  bar) or one of the keywords "then", "else" or "in", then a space and a
 *  backslash is added, but no semi-colon.
 *
 *  @item
 *  The dollar sign character is doubled, unless it immediately precedes an
 *  opening parenthesis or the single character make macros '*', '<', '@@',
 *  '?' or '%'.  Other single character make macros that do not have enclosing
 *  parentheses will fail.  For shell usage of the "$@@", "$?" and "$*"
 *  macros, you must enclose them with curly braces, e.g., "$@{?@}".
 *  The ksh construct @code{$(<command>)} will not work.  Though some
 *  @command{make}s accept @code{$@{var@}} constructs, this function will
 *  assume it is for shell interpretation and double the dollar character.
 *  You must use @code{$(var)} for all @command{make} substitutions.
 *
 *  @item
 *  Double dollar signs are replaced by four before the next character
 *  is examined.
 *
 *  @item
 *  Every line is prefixed with a tab, unless the first line
 *  already starts with a tab.
 *
 *  @item
 *  The newline character on the last line, if present, is suppressed.
 *
 *  @item
 *  Blank lines are stripped.
 *
 *  @item
 *  Lines starting with "@@ifdef", "@@ifndef", "@@else" and "@@endif" are
 *  presumed to be autoconf "sed" expression tags.  These lines will be
 *  emitted as-is, with no tab prefix and no line splicing backslash.
 *  These lines can then be processed at configure time with
 *  @code{AC_CONFIG_FILES} sed expressions, similar to:
 *
 *  @example
 *  sed "/^@@ifdef foo/d;/^@@endif foo/d;/^@@ifndef foo/,/^@@endif foo/d"
 *  @end example
 *  @end enumerate
 *
 *  @noindent
 *  This function is intended to be used approximately as follows:
 *
 *  @example
 *  $(TARGET) : $(DEPENDENCIES)
 *  <+ (out-push-new) +>
 *  ....mostly arbitrary shell script text....
 *  <+ (makefile-script (out-pop #t)) +>
 *  @end example
=*/
SCM
ag_scm_makefile_script(SCM text_scm)
{
    char * res_str; /*@< result string */
    char * out;     /*@< output scanning ptr */
    char * bol;     /*@< start of last output line */
    char   tabch;   /*@< char to use for start-of-line tab */

    char * text = ag_scm2zchars(text_scm, "make script");
    size_t sz   = script_size(&text, &tabch);

    if (sz == 0)
        return scm_from_latin1_string(zNil);

    bol = out = res_str = scribble_get((ssize_t)sz);

    /*
     *  Force the initial line to start with a real tab.
     */
    *(out++) = TAB;

    for (;;) {
        char * p = BRK_MAKE_SCRIPT_CHARS(text);
        size_t l = (size_t)(p - text);
        if (l > 0) {
            memcpy(out, text, l);
            text  = p;
            out += l;
        }

        /*
         * "text" now points to one of three characters:
         * a newline, a dollar or a NUL byte.
         */
        if (*text == NUL)
            break;

        if (*text == NL) {
            if (! handle_eol(&text, &out, tabch, bol))
                break;

            bol = out;

            /*
             * As a special "hack", if a line starts with "@ifdef", "@ifndef",
             * "@else" or "@endif", then we assume post processing sed will
             * fix it up.  Those lines get left alone.
             */
            if (*text == '@') {
                if (handle_sed_expr(&text, &out))
                    bol = out;
            }

        } else {
            /*
             * Quadruple a double dollar, leave alone make-interesting
             * dollars, and double it otherwise.
             */
            switch (text[1]) {
            case '(': case '*': case '@': case '<': case '%': case '?':
                /* one only */
                break;

            case '$':
                /*
                 *  $$ in the shell means process id.  Avoid having to do a
                 *  backward scan on the second '$' by handling the next '$'
                 *  now.  We get FOUR '$' chars.
                 */
                text++;
                *(out++) = '$';
                *(out++) = '$';
                *(out++) = '$';
                /* quadruple */
                break;

            default:
                *(out++) = '$'; /* double */
            }

            *(out++) = *(text++);
        }
    }

    {
        SCM res = scm_from_latin1_stringn(res_str, (size_t)(out - res_str));
        return res;
    }
}

/**
 * @}
 *
 * Local Variables:
 * mode: C
 * c-file-style: "stroustrup"
 * indent-tabs-mode: nil
 * End:
 * end of agen5/expMake.c */
