/* * Copyright 1993-2002 Christopher Seiwald and Perforce Software, Inc. * * This file is part of Jam - see jam.c for Copyright information. */ # include "jam.h" # include "lists.h" # include "variable.h" # include "expand.h" # include "pathsys.h" # include "newstr.h" # include # include # include # ifdef OS_CYGWIN # include # include # endif /* * expand.c - expand a buffer, given variable values * * External routines: * * var_expand() - variable-expand input string into list of strings * * Internal routines: * * var_edit_parse() - parse : modifiers into PATHNAME structure. * var_edit_file() - copy input target name to output, modifying filename. * var_edit_shift() - do upshift/downshift mods. * * 01/25/94 (seiwald) - $(X)$(UNDEF) was expanding like plain $(X) * 04/13/94 (seiwald) - added shorthand L0 for null list pointer * 01/11/01 (seiwald) - added support for :E=emptyvalue, :J=joinval */ typedef struct { PATHNAME f; /* :GDBSMR -- pieces */ char parent; /* :P -- go to parent directory */ char filemods; /* one of the above applied */ char downshift; /* :L -- downshift result */ char upshift; /* :U -- upshift result */ char to_slashes; /* :T -- convert "\" to "/" */ char to_windows; /* :W -- convert cygwin to native paths */ PATHPART empty; /* :E -- default for empties */ PATHPART join; /* :J -- join list with char */ } VAR_EDITS ; static void var_edit_parse( char * mods, VAR_EDITS * edits ); static void var_edit_file ( char * in, string * out, VAR_EDITS * edits ); static void var_edit_shift( string * out, VAR_EDITS * edits ); #define MAGIC_COLON '\001' #define MAGIC_LEFT '\002' #define MAGIC_RIGHT '\003' /* * var_expand() - variable-expand input string into list of strings. * * Would just copy input to output, performing variable expansion, except that * since variables can contain multiple values the result of variable expansion * may contain multiple values (a list). Properly performs "product" operations * that occur in "$(var1)xxx$(var2)" or even "$($(var2))". * * Returns a newly created list. */ LIST * var_expand( LIST * l, char * in, char * end, LOL * lol, int cancopyin ) { char out_buf[ MAXSYM ]; string buf[ 1 ]; string out1[ 1 ]; /* temporary buffer */ size_t prefix_length; char * out; char * inp = in; char * ov; /* for temp copy of variable in outbuf */ int depth; if ( DEBUG_VAREXP ) printf( "expand '%.*s'\n", end - in, in ); /* This gets a lot of cases: $(<) and $(>). */ if ( ( in[ 0 ] == '$' ) && ( in[ 1 ] == '(' ) && ( in[ 3 ] == ')' ) && ( in[ 4 ] == '\0' ) ) { switch ( in[ 2 ] ) { case '<': return list_copy( l, lol_get( lol, 0 ) ); case '>': return list_copy( l, lol_get( lol, 1 ) ); case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return list_copy( l, lol_get( lol, in[ 2 ] - '1' ) ); } } else if ( in[0] == '$' && in[1] == '(' && in[2] == '1' && in[4] == ')' && in[5] == '\0') { switch( in[3] ) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return list_copy( l, lol_get( lol, in[3]-'0'+10-1 ) ); } } /* Expand @() files, to single item plus accompanying file. */ if ( ( in[ 0 ] == '@' ) && ( in[ 1 ] == '(' ) && ( *( end - 1 ) == ')' ) ) { /* We try the expansion until it fits within the propective output * buffer. */ char * at_buf = 0; int at_size = MAXJPATH; int at_len = 0; do { BJAM_FREE( at_buf ); at_buf = (char *)BJAM_MALLOC_ATOMIC( at_size + 1 ); at_len = var_string( in, at_buf, at_size, lol ); at_size *= 2; } while ( ( at_len < 0 ) && ( at_size < INT_MAX / 2 ) ); /* Return the result as a single item list. */ if ( at_len > 0 ) { LIST * r; string_copy( buf, at_buf ); r = list_new( l, newstr( buf->value ) ); string_free( buf ); BJAM_FREE( at_buf ); return r; } BJAM_FREE( at_buf ); } /* Just try simple copy of in to out. */ while ( in < end ) if ( ( *in++ == '$' ) && ( *in == '(' ) ) goto expand; /* No variables expanded - just add copy of input string to list. */ /* 'cancopyin' is an optimization: if the input was already a list item, we * can use copystr() to put it on the new list. Otherwise, we use the slower * newstr(). */ if ( cancopyin ) return list_new( l, copystr( inp ) ); { LIST * r; string_new( buf ); string_append_range( buf, inp, end ); r = list_new( l, newstr( buf->value ) ); string_free( buf ); return r; } expand: string_new( buf ); string_append_range( buf, inp, in - 1 ); /* Copy the part before '$'. */ /* * Input so far (ignore blanks): * * stuff-in-outbuf $(variable) remainder * ^ ^ * in end * Output so far: * * stuff-in-outbuf $ * ^ ^ * out_buf out * * * We just copied the $ of $(...), so back up one on the output. We now find * the matching close paren, copying the variable and modifiers between the * $( and ) temporarily into out_buf, so that we can replace :'s with * MAGIC_COLON. This is necessary to avoid being confused by modifier values * that are variables containing :'s. Ugly. */ depth = 1; inp = ++in; /* Skip over the '('. */ while ( ( in < end ) && depth ) { switch ( *in++ ) { case '(': ++depth; break; case ')': --depth; break; } } /* * Input so far (ignore blanks): * * stuff-in-outbuf $(variable) remainder * ^ ^ ^ * inp in end */ prefix_length = buf->size; string_append_range( buf, inp, in - 1 ); out = buf->value + prefix_length; for ( ov = out; ov < buf->value + buf->size; ++ov ) { switch ( *ov ) { case ':': *ov = MAGIC_COLON; break; case '[': *ov = MAGIC_LEFT ; break; case ']': *ov = MAGIC_RIGHT; break; } } /* * Input so far (ignore blanks): * * stuff-in-outbuf $(variable) remainder * ^ ^ * in end * Output so far: * * stuff-in-outbuf variable * ^ ^ ^ * out_buf out ov * * Later we will overwrite 'variable' in out_buf, but we will be done with * it by then. 'variable' may be a multi-element list, so may each value for * '$(variable element)', and so may 'remainder'. Thus we produce a product * of three lists. */ { LIST * variables = 0; LIST * remainder = 0; LIST * vars; /* Recursively expand variable name & rest of input. */ if ( out < ov ) variables = var_expand( L0, out, ov, lol, 0 ); if ( in < end ) remainder = var_expand( L0, in, end, lol, 0 ); /* Now produce the result chain. */ /* For each variable name. */ for ( vars = variables; vars; vars = list_next( vars ) ) { LIST * value = 0; LIST * evalue = 0; char * colon; char * bracket; string variable[1]; char * varname; int sub1 = 0; int sub2 = -1; VAR_EDITS edits; /* Look for a : modifier in the variable name. Must copy into * varname so we can modify it. */ string_copy( variable, vars->string ); varname = variable->value; if ( ( colon = strchr( varname, MAGIC_COLON ) ) ) { string_truncate( variable, colon - varname ); var_edit_parse( colon + 1, &edits ); } /* Look for [x-y] subscripting. sub1 and sub2 are x and y. */ if ( ( bracket = strchr( varname, MAGIC_LEFT ) ) ) { /* Make all syntax errors in [] subscripting result in the same * behavior: silenty return an empty expansion (by setting sub2 * = 0). Brute force parsing; May get moved into yacc someday. */ char * s = bracket + 1; string_truncate( variable, bracket - varname ); do /* so we can use "break" */ { /* Allow negative indexes. */ if ( !isdigit( *s ) && ( *s != '-' ) ) { sub2 = 0; break; } sub1 = atoi( s ); /* Skip over the first symbol, which is either a digit or dash. */ ++s; while ( isdigit( *s ) ) ++s; if ( *s == MAGIC_RIGHT ) { sub2 = sub1; break; } if ( *s != '-' ) { sub2 = 0; break; } ++s; if ( *s == MAGIC_RIGHT ) { sub2 = -1; break; } if ( !isdigit( *s ) && ( *s != '-' ) ) { sub2 = 0; break; } /* First, compute the index of the last element. */ sub2 = atoi( s ); while ( isdigit( *++s ) ); if ( *s != MAGIC_RIGHT ) sub2 = 0; } while ( 0 ); /* Anything but the end of the string, or the colon introducing * a modifier is a syntax error. */ ++s; if ( *s && ( *s != MAGIC_COLON ) ) sub2 = 0; *bracket = '\0'; } /* Get variable value, with special handling for $(<), $(>), $(n). */ if ( !varname[1] ) { if ( varname[0] == '<' ) value = lol_get( lol, 0 ); else if ( varname[0] == '>' ) value = lol_get( lol, 1 ); else if ( ( varname[0] >= '1' ) && ( varname[0] <= '9' ) ) value = lol_get( lol, varname[0] - '1' ); else if( varname[0] == '1' && varname[1] >= '0' && varname[1] <= '9' && !varname[2] ) value = lol_get( lol, varname[1] - '0' + 10 - 1 ); } if ( !value ) value = var_get( varname ); /* Handle negitive indexes: part two. */ { int length = list_length( value ); if ( sub1 < 0 ) sub1 = length + sub1; else sub1 -= 1; if ( sub2 < 0 ) sub2 = length + 1 + sub2 - sub1; else sub2 -= sub1; /* The "sub2 < 0" test handles the semantic error of sub2 < * sub1. */ if ( sub2 < 0 ) sub2 = 0; } /* The fast path: $(x) - just copy the variable value. This is only * an optimization. */ if ( ( out == out_buf ) && !bracket && !colon && ( in == end ) ) { string_free( variable ); l = list_copy( l, value ); continue; } /* Handle start subscript. */ while ( ( sub1 > 0 ) && value ) --sub1, value = list_next( value ); /* Empty w/ :E=default?. */ if ( !value && colon && edits.empty.ptr ) evalue = value = list_new( L0, newstr( edits.empty.ptr ) ); /* For each variable value. */ string_new( out1 ); for ( ; value; value = list_next( value ) ) { LIST * rem; size_t postfix_start; /* Handle end subscript (length actually). */ if ( sub2 >= 0 && --sub2 < 0 ) break; string_truncate( buf, prefix_length ); /* Apply : mods, if present */ if ( colon && edits.filemods ) var_edit_file( value->string, out1, &edits ); else string_append( out1, value->string ); if ( colon && ( edits.upshift || edits.downshift || edits.to_slashes || edits.to_windows ) ) var_edit_shift( out1, &edits ); /* Handle :J=joinval */ /* If we have more values for this var, just keep appending them * (using the join value) rather than creating separate LIST * elements. */ if ( colon && edits.join.ptr && ( list_next( value ) || list_next( vars ) ) ) { string_append( out1, edits.join.ptr ); continue; } string_append( buf, out1->value ); string_free( out1 ); string_new( out1 ); /* If no remainder, append result to output chain. */ if ( in == end ) { l = list_new( l, newstr( buf->value ) ); continue; } /* For each remainder, append the complete string to the output * chain. Remember the end of the variable expansion so we can * just tack on each instance of 'remainder'. */ postfix_start = buf->size; for ( rem = remainder; rem; rem = list_next( rem ) ) { string_truncate( buf, postfix_start ); string_append( buf, rem->string ); l = list_new( l, newstr( buf->value ) ); } } string_free( out1 ); /* Toss used empty. */ if ( evalue ) list_free( evalue ); string_free( variable ); } /* variables & remainder were gifts from var_expand and must be freed. */ if ( variables ) list_free( variables ); if ( remainder ) list_free( remainder ); if ( DEBUG_VAREXP ) { printf( "expanded to " ); list_print( l ); printf( "\n" ); } string_free( buf ); return l; } } /* * var_edit_parse() - parse : modifiers into PATHNAME structure * * The : modifiers in a $(varname:modifier) currently support replacing or * omitting elements of a filename, and so they are parsed into a PATHNAME * structure (which contains pointers into the original string). * * Modifiers of the form "X=value" replace the component X with the given value. * Modifiers without the "=value" cause everything but the component X to be * omitted. X is one of: * * G * D directory name * B base name * S .suffix * M (member) * R root directory - prepended to whole path * * This routine sets: * * f->f_xxx.ptr = 0 * f->f_xxx.len = 0 * -> leave the original component xxx * * f->f_xxx.ptr = string * f->f_xxx.len = strlen( string ) * -> replace component xxx with string * * f->f_xxx.ptr = "" * f->f_xxx.len = 0 * -> omit component xxx * * var_edit_file() below and path_build() obligingly follow this convention. */ static void var_edit_parse( char * mods, VAR_EDITS * edits ) { int havezeroed = 0; memset( (char *)edits, 0, sizeof( *edits ) ); while ( *mods ) { char * p; PATHPART * fp; switch ( *mods++ ) { case 'L': edits->downshift = 1; continue; case 'U': edits->upshift = 1; continue; case 'P': edits->parent = edits->filemods = 1; continue; case 'E': fp = &edits->empty; goto strval; case 'J': fp = &edits->join; goto strval; case 'G': fp = &edits->f.f_grist; goto fileval; case 'R': fp = &edits->f.f_root; goto fileval; case 'D': fp = &edits->f.f_dir; goto fileval; case 'B': fp = &edits->f.f_base; goto fileval; case 'S': fp = &edits->f.f_suffix; goto fileval; case 'M': fp = &edits->f.f_member; goto fileval; case 'T': edits->to_slashes = 1; continue; case 'W': edits->to_windows = 1; continue; default: return; /* Should complain, but so what... */ } fileval: /* Handle :CHARS, where each char (without a following =) selects a * particular file path element. On the first such char, we deselect all * others (by setting ptr = "", len = 0) and for each char we select * that element (by setting ptr = 0). */ edits->filemods = 1; if ( *mods != '=' ) { if ( !havezeroed++ ) { int i; for ( i = 0; i < 6; ++i ) { edits->f.part[ i ].len = 0; edits->f.part[ i ].ptr = ""; } } fp->ptr = 0; continue; } strval: /* Handle :X=value, or :X */ if ( *mods != '=' ) { fp->ptr = ""; fp->len = 0; } else if ( ( p = strchr( mods, MAGIC_COLON ) ) ) { *p = 0; fp->ptr = ++mods; fp->len = p - mods; mods = p + 1; } else { fp->ptr = ++mods; fp->len = strlen( mods ); mods += fp->len; } } } /* * var_edit_file() - copy input target name to output, modifying filename. */ static void var_edit_file( char * in, string * out, VAR_EDITS * edits ) { PATHNAME pathname; /* Parse apart original filename, putting parts into "pathname". */ path_parse( in, &pathname ); /* Replace any pathname with edits->f */ if ( edits->f.f_grist .ptr ) pathname.f_grist = edits->f.f_grist; if ( edits->f.f_root .ptr ) pathname.f_root = edits->f.f_root; if ( edits->f.f_dir .ptr ) pathname.f_dir = edits->f.f_dir; if ( edits->f.f_base .ptr ) pathname.f_base = edits->f.f_base; if ( edits->f.f_suffix.ptr ) pathname.f_suffix = edits->f.f_suffix; if ( edits->f.f_member.ptr ) pathname.f_member = edits->f.f_member; /* If requested, modify pathname to point to parent. */ if ( edits->parent ) path_parent( &pathname ); /* Put filename back together. */ path_build( &pathname, out, 0 ); } /* * var_edit_shift() - do upshift/downshift mods. */ static void var_edit_shift( string * out, VAR_EDITS * edits ) { /* Handle upshifting, downshifting and slash translation now. */ char * p; for ( p = out->value; *p; ++p) { if ( edits->upshift ) *p = toupper( *p ); else if ( edits->downshift ) *p = tolower( *p ); if ( edits->to_slashes && ( *p == '\\' ) ) *p = '/'; # ifdef OS_CYGWIN if ( edits->to_windows ) { char result[ MAX_PATH + 1 ]; cygwin_conv_to_win32_path( out->value, result ); assert( strlen( result ) <= MAX_PATH ); string_free( out ); string_copy( out, result ); } # endif } out->size = p - out->value; } #ifndef NDEBUG void var_expand_unit_test() { LOL lol[ 1 ]; LIST * l; LIST * l2; LIST * expected = list_new( list_new( L0, newstr( "axb" ) ), newstr( "ayb" ) ); LIST * e2; char axyb[] = "a$(xy)b"; char azb[] = "a$($(z))b"; char path[] = "$(p:W)"; # ifdef OS_CYGWIN char cygpath[ 256 ]; cygwin_conv_to_posix_path( "c:\\foo\\bar", cygpath ); # else char cygpath[] = "/cygdrive/c/foo/bar"; # endif lol_init(lol); var_set( "xy", list_new( list_new( L0, newstr( "x" ) ), newstr( "y" ) ), VAR_SET ); var_set( "z", list_new( L0, newstr( "xy" ) ), VAR_SET ); var_set( "p", list_new( L0, newstr( cygpath ) ), VAR_SET ); l = var_expand( 0, axyb, axyb + sizeof( axyb ) - 1, lol, 0 ); for ( l2 = l, e2 = expected; l2 && e2; l2 = list_next( l2 ), e2 = list_next( e2 ) ) assert( !strcmp( e2->string, l2->string ) ); assert( l2 == 0 ); assert( e2 == 0 ); list_free( l ); l = var_expand( 0, azb, azb + sizeof( azb ) - 1, lol, 0 ); for ( l2 = l, e2 = expected; l2 && e2; l2 = list_next( l2 ), e2 = list_next( e2 ) ) assert( !strcmp( e2->string, l2->string ) ); assert( l2 == 0 ); assert( e2 == 0 ); list_free( l ); l = var_expand( 0, path, path + sizeof( path ) - 1, lol, 0 ); assert( l != 0 ); assert( list_next( l ) == 0 ); # ifdef OS_CYGWIN /* On some installations of cygwin the drive letter is expanded to other * case. This has been reported to be the case if cygwin has been installed * to C:\ as opposed to C:\cygwin. Since case of the drive letter will not * matter, we allow for both. */ assert( !strcmp( l->string, "c:\\foo\\bar" ) || !strcmp( l->string, "C:\\foo\\bar" ) ); # else assert( !strcmp( l->string, cygpath ) ); # endif list_free( l ); list_free( expected ); lol_free( lol ); } #endif