Last update June 8, 2007

Daniel Keep /
shfmt



A Shell/PHP-style formatter using inline variable expansion. See down the bottom of the file for an example.

/**
 * Shell/PHP style formatter.
 * Written by Daniel Keep, 2007.
 * Released under the BSDv2 license.
 */
/*
  Copyright © 2007, Daniel Keep
  All rights reserved.
  
  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are met:
  
  * Redistributions of source code must retain the above copyright
  notice, this list of conditions and the following disclaimer.
  * Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
  * The names of the contributors may be used to endorse or promote
  products derived from this software without specific prior written
  permission.
  
  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  POSSIBILITY OF SUCH DAMAGE.
*/
module shfmt;

/*
 * BNF for expansions:
 *
 * Expansion ::= "$" ExpansionSpec
 * 
 * ExpansionSpec ::= FormattingOptions ExpansionExpr
 * ExpansionSpec ::= ExpansionExpr
 * 
 * FormattingOptions ::= "(" string ")"
 * 
 * ExpansionExpr ::= "{" D_Expression "}"
 * ExpansionExpr ::= D_Identifier
 */

import std.stdio : writefln;
import std.string : format;

/**
 * Determines if the given character is a valid D identifier character.  If it
 * must be a valid starting character, you should specify first as true.
 *
 * Compatible with CTFE.
 */
private bool isIdentChar(dchar c, bool first=false)
{
    if( 'a' <= c && c <= 'z'
            || 'A' <= c && c <= 'Z'
            || c == '_'
            || 0xa0 <= c && c < 0xd800
            || 0xdfff < c
            || !first && ('0' <= c && c <= '9') )
        return true;
    else
        return false;
}

/**
 * Expands the given format string.
 *
 * Compatible with CTFE.
 */
char[] _F(char[] fs)
{
    return "_F_join(" ~ _F_raw(fs) ~ ")";
}

/**
 * List of internal states _F_raw can be in.
 */
private enum FState
{
    Default,        /// just spitting stuff out
    PostDollar,     /// immediately after a '$'
    Options,        /// inside format options (...)
    PostOptions,    /// immediately after format options
    Expression,     /// inside a complex expression {...}
    Varname         /// a regular identifier
}

/**
 * Expands the given format string into a comma-delimited list of string
 * components.
 *
 * Compatible with CTFE.
 */
char[] _F_raw(char[] fs)
{
    char[] result;  /// result accumulator
    FState state;   /// current state

    /// This is used to track how many nested braces deep we are within a
    /// complex expression we are.  It gets incremented when we see a '{', and
    /// decremented when we see a '}'.  Once depth == 0, we're finished.
    uint depth;

    /// Used to build up the formatting options list.  If this is empty, it is
    /// equivalent to "s".
    char[] opt;

    /* Assume the first component is a literal.  Empty strings are basically
     * harmless.
     */
    result ~= "\"";

    /* BUG: this should iterate over the format string using dchars, but
     * automatic unpacking isn't supported in CTFE as of dmd v1.014.
     */
    foreach( char c ; fs )
    {
        // Default state
        if( state == FState.Default )
        {
            if( c == '$' )
                // Start of a variable expansion
                state = FState.PostDollar;
            else if( c == '"' )
            {
                // Make sure to escape double quotes.
                result ~= '\\';
                result ~= c;
            }
            else
                // Just output this character literally.
                result ~= c;
        }

        // Start of a variable expansion
        else if( state == FState.PostDollar )
        {
            if( c == '$' )
            {
                // Escaped '$'
                state = FState.Default;
                result ~= c;
            }
            else if( c == '(' )
                // Start of option list
                state = FState.Options;

            else if( c == '{' )
            {
                // Start of a complex expression
                state = FState.Expression;
                depth = 1;
                result ~= "\",format(\"%s\",(";
            }
            else if( isIdentChar(c, true) )
            {
                // Start of a simple identifier
                state = FState.Varname;
                result ~= "\",format(\"%s\",(";
                result ~= c;
            }
            else
            {
                /* Something else; just output the $ and this character
                 * literally.
                 */
                state = FState.Default;
                result ~= "$";
                result ~= c;
            }
        }

        // Inside format options (...)
        else if( state == FState.Options )
        {
            if( c == ')' )
                // That's all, folks!
                state = FState.PostOptions;

            else
                // Append character to options
                opt ~= c;
        }

        // Immediately after format options
        else if( state == FState.PostOptions )
        {
            if( c == '{' )
            {
                // Starting a complex expression
                state = FState.Expression;
                depth = 1;
                if( opt.length == 0 )
                    opt = "s";
                result ~= "\",format(\"%"~opt~"\",(";
                opt = null;
            }
            else if( isIdentChar(c, true) )
            {
                // Just a regular identifier
                state = FState.Varname;
                if( opt.length == 0 )
                    opt = "s";
                result ~= "\",format(\"%"~opt~"\",(";
                opt = null;
                result ~= c;
            }
            else
                // Malformed format string
                assert(false, "Invalid format string: "~fs);
        }

        // Within a complex expression
        else if( state == FState.Expression )
        {
            if( c == '{' )
            {
                // Nest another level
                ++depth;
                result ~= c;
            }
            else if( c == '}' )
            {
                // Leaving current level.
                --depth;
                if( depth == 0 )
                {
                    // We just closed the initial opening brace; we're done
                    state = FState.Default;
                    result ~= ")),\"";
                }
                else
                    result ~= c;
            }
            else
                result ~= c;
        }

        // A standard identifier
        else if( state == FState.Varname )
        {
            if( isIdentChar(c) )
            {
                result ~= c;
            }
            else
            {
                state = FState.Default;
                result ~= ")),\"";
                result ~= c;
            }
        }
        else
            // Unknown state; now how'd that happen?
            assert(false);
    }
    
    if( state == FState.Default )
        // Close current literal string
        result ~= "\"";
    else if( state == FState.PostDollar )
        // Format string ended with a '$': that's cool.
        result ~= "$\"";
    else if( state == FState.Varname )
        // Finish the ending variable expansion
        result ~= "))";
    else
        // Malformed format string
        assert(false);

    return result;
}

/**
 * This function takes several separate strings and joins them together
 * efficiently.
 */
char[] _F_join(char[][] parts...)
{
    char[] result;
    uint acc;
    
    // Work out how much room we'll need
    foreach( part ; parts )
        acc += part.length;
    
    if( acc == 0 )
        return null;

    // Stuff the parts together into the result
    result.length = acc;
    acc = 0;

    foreach( part ; parts )
    {
        if( part.length == 0 )
            continue;

        result[acc..acc+part.length] = part[];
        acc += part.length;
    }

    return result;
}

/**
 * Provides a simple wrapper around writefln and _F.
 */
char[] writeFln(char[] fs)
{
    return "writefln(\"%s\"," ~ _F(fs) ~ ");";
}

version( shfmt_test ):

    void main()
    {
        // Prints:
        // foo = 13
        // bar = 29
        // foo + bar = 42
        // .. in hex = 2a

        int foo = 13;
        int bar = 29;
        mixin(writeFln("foo = $foo"));
        mixin(writeFln("bar = $bar"));
        mixin(writeFln("foo + bar = ${foo+bar}"));
        mixin(writeFln(".. in hex = $(x){foo+bar}"));
    }

FrontPage | News | TestPage | MessageBoard | Search | Contributors | Folders | Index | Help | Preferences | Edit

Edit text of this page (date of last change: June 8, 2007 15:03 (diff))