Skip to content

Instantly share code, notes, and snippets.

@gromnitsky
Last active July 9, 2021 05:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gromnitsky/e39a90811ee478c4ae9b38c490e80d78 to your computer and use it in GitHub Desktop.
Save gromnitsky/e39a90811ee478c4ae9b38c490e80d78 to your computer and use it in GitHub Desktop.
An extract of patsubst function from GNU Make
/*
An extract of patsubst function from GNU Make 4.2.90
(6f339b22eb87dc80d1037ccb04c787d156db0e8f)
$ cc gmake-patsubst.c -o gmake-patsubst
$ ./gmake-patsubst %.c %.o 'foo.c bar.c'
foo.o bar.o
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <err.h>
#include <ctype.h>
#include <getopt.h>
/* not demo related */
int conf_verbose = 0;
extern const char *__progname; /* glibc */
void
msg(const char *fmt, ...)
{
if (!conf_verbose) return;
va_list ap;
va_start(ap, fmt);
printf("%s: ", __progname);
vprintf(fmt, ap);
va_end(ap);
}
/* gmake code */
#define UCHAR_MAX 255
#define MAP_NUL 0x0001
#define MAP_BLANK 0x0002
#define MAP_NEWLINE 0x0004
#define MAP_COMMENT 0x0008
#define MAP_SEMI 0x0010
#define MAP_EQUALS 0x0020
#define MAP_COLON 0x0040
#define MAP_PERCENT 0x0080
#define MAP_PIPE 0x0100
#define MAP_DOT 0x0200
#define MAP_COMMA 0x0400
/* These are the valid characters for a user-defined function. */
#define MAP_USERFUNC 0x2000
/* This means not only a '$', but skip the variable reference. */
#define MAP_VARIABLE 0x4000
/* The set of characters which are directory separators is OS-specific. */
#define MAP_DIRSEP 0x8000
#define MAP_SPACE (MAP_BLANK|MAP_NEWLINE)
unsigned short stopchar_map[UCHAR_MAX + 1] = {0};
#define ANY_SET(_v,_m) (((_v)&(_m)) != 0)
#define STOP_SET(_v,_m) ANY_SET(stopchar_map[(unsigned char)(_v)],(_m))
#define NONE_SET(_v,_m) (! ANY_SET ((_v),(_m)))
#define ISBLANK(c) STOP_SET((c),MAP_BLANK)
#define ISSPACE(c) STOP_SET((c),MAP_SPACE)
#define NEXT_TOKEN(s) while (ISSPACE (*(s))) ++(s)
#define END_OF_TOKEN(s) while (! STOP_SET (*(s), MAP_SPACE|MAP_NUL)) ++(s)
#define strneq(a, b, l) (strncmp ((a), (b), (l)) == 0)
/* Search STRING for an unquoted STOPCHAR or blank (if BLANK is nonzero).
Backslashes quote STOPCHAR, blanks if BLANK is nonzero, and backslash.
Quoting backslashes are removed from STRING by compacting it into
itself. Returns a pointer to the first unquoted STOPCHAR if there is
one, or nil if there are none. STOPCHARs inside variable references are
ignored if IGNOREVARS is true.
STOPCHAR _cannot_ be '$' if IGNOREVARS is true. */
static char *
find_char_unquote (char *string, int map)
{
unsigned int string_len = 0;
char *p = string;
/* Always stop on NUL. */
map |= MAP_NUL;
while (1)
{
while (! STOP_SET (*p, map))
++p;
if (*p == '\0')
break;
/* If we stopped due to a variable reference, skip over its contents. */
if (STOP_SET (*p, MAP_VARIABLE))
{
char openparen = p[1];
/* Check if '$' is the last character in the string. */
if (openparen == '\0')
break;
p += 2;
/* Skip the contents of a non-quoted, multi-char variable ref. */
if (openparen == '(' || openparen == '{')
{
unsigned int pcount = 1;
char closeparen = (openparen == '(' ? ')' : '}');
while (*p)
{
if (*p == openparen)
++pcount;
else if (*p == closeparen)
if (--pcount == 0)
{
++p;
break;
}
++p;
}
}
/* Skipped the variable reference: look for STOPCHARS again. */
continue;
}
if (p > string && p[-1] == '\\')
{
/* Search for more backslashes. */
int i = -2;
while (&p[i] >= string && p[i] == '\\')
--i;
++i;
/* Only compute the length if really needed. */
if (string_len == 0)
string_len = strlen (string);
/* The number of backslashes is now -I.
Copy P over itself to swallow half of them. */
memmove (&p[i], &p[i/2], (string_len - (p - string)) - (i/2) + 1);
p += i/2;
if (i % 2 == 0)
/* All the backslashes quoted each other; the STOPCHAR was
unquoted. */
return p;
/* The STOPCHAR was quoted by a backslash. Look for another. */
}
else
/* No backslash in sight. */
return p;
}
/* Never hit a STOPCHAR or blank (with BLANK nonzero). */
return 0;
}
/* Search PATTERN for an unquoted % and handle quoting. */
char *
find_percent (char *pattern)
{
return find_char_unquote (pattern, MAP_PERCENT);
}
/* Return the address of the first whitespace or null in the string S. */
char *
end_of_token (const char *s)
{
END_OF_TOKEN (s);
return (char *)s;
}
/* Return the address of the first nonwhitespace or null in the string S. */
char *
next_token (const char *s)
{
NEXT_TOKEN (s);
return (char *)s;
}
/* Find the next token in PTR; return the address of it, and store the length
of the token into *LENGTHPTR if LENGTHPTR is not nil. Set *PTR to the end
of the token, so this function can be called repeatedly in a loop. */
char *
find_next_token (const char **ptr, unsigned int *lengthptr)
{
const char *p = next_token (*ptr);
if (*p == '\0')
return 0;
*ptr = end_of_token (p);
if (lengthptr != 0)
*lengthptr = *ptr - p;
return (char *)p;
}
void *
xrealloc (void *ptr, unsigned int size)
{
void *result;
/* Some older implementations of realloc() don't conform to ISO. */
if (! size)
size = 1;
result = ptr ? realloc (ptr, size) : malloc (size);
if (result == 0)
errx(1, "no memory");
return result;
}
void *
xmalloc (unsigned int size)
{
/* Make sure we don't allocate 0, for pre-ISO implementations. */
void *result = malloc (size ? size : 1);
if (result == 0)
errx(1, "no memory");
return result;
}
/* The next two describe the variable output buffer.
This buffer is used to hold the variable-expansion of a line of the
makefile. It is made bigger with realloc whenever it is too small.
variable_buffer_length is the size currently allocated.
variable_buffer is the address of the buffer.
For efficiency, it's guaranteed that the buffer will always have
VARIABLE_BUFFER_ZONE extra bytes allocated. This allows you to add a few
extra chars without having to call a function. Note you should never use
these bytes unless you're _sure_ you have room (you know when the buffer
length was last checked. */
#define VARIABLE_BUFFER_ZONE 5
static unsigned int variable_buffer_length;
char *variable_buffer;
/* Subroutine of variable_expand and friends:
The text to add is LENGTH chars starting at STRING to the variable_buffer.
The text is added to the buffer at PTR, and the updated pointer into
the buffer is returned as the value. Thus, the value returned by
each call to variable_buffer_output should be the first argument to
the following call. */
char *
variable_buffer_output (char *ptr, const char *string, unsigned int length)
{
register unsigned int newlen = length + (ptr - variable_buffer);
if ((newlen + VARIABLE_BUFFER_ZONE) > variable_buffer_length)
{
unsigned int offset = ptr - variable_buffer;
variable_buffer_length = (newlen + 100 > 2 * variable_buffer_length
? newlen + 100
: 2 * variable_buffer_length);
variable_buffer = xrealloc (variable_buffer, variable_buffer_length);
ptr = variable_buffer + offset;
}
memcpy (ptr, string, length);
return ptr + length;
}
/* Store into VARIABLE_BUFFER at O the result of scanning TEXT and replacing
each occurrence of SUBST with REPLACE. TEXT is null-terminated. SLEN is
the length of SUBST and RLEN is the length of REPLACE. If BY_WORD is
nonzero, substitutions are done only on matches which are complete
whitespace-delimited words. */
char *
subst_expand (char *o, const char *text, const char *subst, const char *replace,
unsigned int slen, unsigned int rlen, int by_word)
{
const char *t = text;
const char *p;
if (slen == 0 && !by_word)
{
/* The first occurrence of "" in any string is its end. */
o = variable_buffer_output (o, t, strlen (t));
if (rlen > 0)
o = variable_buffer_output (o, replace, rlen);
return o;
}
do
{
if (by_word && slen == 0)
/* When matching by words, the empty string should match
the end of each word, rather than the end of the whole text. */
p = end_of_token (next_token (t));
else
{
p = strstr (t, subst);
if (p == 0)
{
/* No more matches. Output everything left on the end. */
o = variable_buffer_output (o, t, strlen (t));
return o;
}
}
/* Output everything before this occurrence of the string to replace. */
if (p > t)
o = variable_buffer_output (o, t, p - t);
/* If we're substituting only by fully matched words,
or only at the ends of words, check that this case qualifies. */
if (by_word
&& ((p > text && !ISSPACE (p[-1]))
|| ! STOP_SET (p[slen], MAP_SPACE|MAP_NUL)))
/* Struck out. Output the rest of the string that is
no longer to be replaced. */
o = variable_buffer_output (o, subst, slen);
else if (rlen > 0)
/* Output the replacement string. */
o = variable_buffer_output (o, replace, rlen);
/* Advance T past the string to be replaced. */
t = p + slen;
} while (*t != '\0');
return o;
}
/* Store into VARIABLE_BUFFER at O the result of scanning TEXT
and replacing strings matching PATTERN with REPLACE.
If PATTERN_PERCENT is not nil, PATTERN has already been
run through find_percent, and PATTERN_PERCENT is the result.
If REPLACE_PERCENT is not nil, REPLACE has already been
run through find_percent, and REPLACE_PERCENT is the result.
Note that we expect PATTERN_PERCENT and REPLACE_PERCENT to point to the
character _AFTER_ the %, not to the % itself.
*/
char *
patsubst_expand_pat (char *o, const char *text,
const char *pattern, const char *replace,
const char *pattern_percent, const char *replace_percent)
{
unsigned int pattern_prepercent_len, pattern_postpercent_len;
unsigned int replace_prepercent_len, replace_postpercent_len;
const char *t;
unsigned int len;
int doneany = 0;
/* Record the length of REPLACE before and after the % so we don't have to
compute these lengths more than once. */
if (replace_percent)
{
replace_prepercent_len = replace_percent - replace - 1;
replace_postpercent_len = strlen (replace_percent);
}
else
{
replace_prepercent_len = strlen (replace);
replace_postpercent_len = 0;
}
if (!pattern_percent)
/* With no % in the pattern, this is just a simple substitution. */
return subst_expand (o, text, pattern, replace,
strlen (pattern), strlen (replace), 1);
/* Record the length of PATTERN before and after the %
so we don't have to compute it more than once. */
pattern_prepercent_len = pattern_percent - pattern - 1;
pattern_postpercent_len = strlen (pattern_percent);
while ((t = find_next_token (&text, &len)) != 0)
{
int fail = 0;
/* Is it big enough to match? */
if (len < pattern_prepercent_len + pattern_postpercent_len)
fail = 1;
/* Does the prefix match? */
if (!fail && pattern_prepercent_len > 0
&& (*t != *pattern
|| t[pattern_prepercent_len - 1] != pattern_percent[-2]
|| !strneq (t + 1, pattern + 1, pattern_prepercent_len - 1)))
fail = 1;
/* Does the suffix match? */
if (!fail && pattern_postpercent_len > 0
&& (t[len - 1] != pattern_percent[pattern_postpercent_len - 1]
|| t[len - pattern_postpercent_len] != *pattern_percent
|| !strneq (&t[len - pattern_postpercent_len],
pattern_percent, pattern_postpercent_len - 1)))
fail = 1;
if (fail)
/* It didn't match. Output the string. */
o = variable_buffer_output (o, t, len);
else
{
/* It matched. Output the replacement. */
/* Output the part of the replacement before the %. */
o = variable_buffer_output (o, replace, replace_prepercent_len);
if (replace_percent != 0)
{
/* Output the part of the matched string that
matched the % in the pattern. */
o = variable_buffer_output (o, t + pattern_prepercent_len,
len - (pattern_prepercent_len
+ pattern_postpercent_len));
/* Output the part of the replacement after the %. */
o = variable_buffer_output (o, replace_percent,
replace_postpercent_len);
}
}
/* Output a space, but not if the replacement is "". */
if (fail || replace_prepercent_len > 0
|| (replace_percent != 0 && len + replace_postpercent_len > 0))
{
o = variable_buffer_output (o, " ", 1);
doneany = 1;
}
}
if (doneany)
/* Kill the last space. */
--o;
return o;
}
/* Store into VARIABLE_BUFFER at O the result of scanning TEXT
and replacing strings matching PATTERN with REPLACE.
If PATTERN_PERCENT is not nil, PATTERN has already been
run through find_percent, and PATTERN_PERCENT is the result.
If REPLACE_PERCENT is not nil, REPLACE has already been
run through find_percent, and REPLACE_PERCENT is the result.
Note that we expect PATTERN_PERCENT and REPLACE_PERCENT to point to the
character _AFTER_ the %, not to the % itself.
*/
char *
patsubst_expand (char *o, const char *text, char *pattern, char *replace)
{
const char *pattern_percent = find_percent (pattern);
const char *replace_percent = find_percent (replace);
/* If there's a percent in the pattern or replacement skip it. */
if (replace_percent)
++replace_percent;
if (pattern_percent)
++pattern_percent;
msg("pattern_percent=`%s`\n", pattern_percent);
msg("replace_percent=`%s`\n", replace_percent);
return patsubst_expand_pat (o, text, pattern, replace,
pattern_percent, replace_percent);
}
/* Return a pointer to the beginning of the variable buffer. */
static char *
initialize_variable_output (void)
{
/* If we don't have a variable output buffer yet, get one. */
if (variable_buffer == 0)
{
variable_buffer_length = 200;
variable_buffer = xmalloc (variable_buffer_length);
variable_buffer[0] = '\0';
}
return variable_buffer;
}
/* This character map locate stop chars when parsing GNU makefiles.
Each element is true if we should stop parsing on that character. */
static void
initialize_stopchar_map (void)
{
int i;
stopchar_map[(int)'\0'] = MAP_NUL;
stopchar_map[(int)'#'] = MAP_COMMENT;
stopchar_map[(int)';'] = MAP_SEMI;
stopchar_map[(int)'='] = MAP_EQUALS;
stopchar_map[(int)':'] = MAP_COLON;
stopchar_map[(int)'%'] = MAP_PERCENT;
stopchar_map[(int)'|'] = MAP_PIPE;
stopchar_map[(int)'.'] = MAP_DOT | MAP_USERFUNC;
stopchar_map[(int)','] = MAP_COMMA;
stopchar_map[(int)'$'] = MAP_VARIABLE;
stopchar_map[(int)'-'] = MAP_USERFUNC;
stopchar_map[(int)'_'] = MAP_USERFUNC;
stopchar_map[(int)' '] = MAP_BLANK;
stopchar_map[(int)'\t'] = MAP_BLANK;
stopchar_map[(int)'/'] = MAP_DIRSEP;
#if defined(VMS)
stopchar_map[(int)':'] |= MAP_DIRSEP;
stopchar_map[(int)']'] |= MAP_DIRSEP;
stopchar_map[(int)'>'] |= MAP_DIRSEP;
#elif defined(HAVE_DOS_PATHS)
stopchar_map[(int)'\\'] |= MAP_DIRSEP;
#endif
for (i = 1; i <= UCHAR_MAX; ++i)
{
if (isspace (i) && NONE_SET (stopchar_map[i], MAP_BLANK))
/* Don't mark blank characters as newline characters. */
stopchar_map[i] |= MAP_NEWLINE;
else if (isalnum (i))
stopchar_map[i] |= MAP_USERFUNC;
}
}
void
usage() {
errx(1, "Usage: gmake_patsubst [-v] pattern replacement text");
}
int main(int argc, char **argv) {
int c;
while ((c = getopt (argc, argv, "v")) != -1) {
c == 'v' ? conf_verbose++ : usage();
}
argc -= optind;
argv += optind;
if (argc != 3) usage();
initialize_stopchar_map();
char *buf = initialize_variable_output();
char *bufstart = buf;
buf = patsubst_expand(buf, argv[2], argv[0], argv[1]);
char *result = xmalloc(variable_buffer_length);
printf(conf_verbose ? "result: `%s`\n" : "%s\n",
strncpy(result, bufstart, buf-bufstart));
return 0;
}
#!/usr/bin/env node
/*
A "port" of GNU Make's patsubst function to nodejs-6.6.
$ ./gmake-patsubst.js %.c %.o 'foo.c bar.c'
foo.o bar.o
*/
let transform = function(ps, pattern, replacement, chunk) {
if (pattern.indexOf('%') === -1)
return simple(pattern, replacement, chunk)
// does the prefix & the suffix match?
if ( (chunk.slice(0, ps.pattern_prefix.length) === ps.pattern_prefix)
&& (chunk.slice(-ps.pattern_suffix.length) === ps.pattern_suffix) ) {
if (replacement.indexOf('%') === -1) return replacement
let stem = chunk.slice(ps.pattern_prefix.length,
-ps.pattern_suffix.length)
return ps.replacement_prefix + stem + ps.replacement_suffix
}
return chunk
}
let simple = function(pattern, replacement, chunk) {
return pattern === chunk ? replacement : chunk
}
let affixes = function(pattern, replacement) {
let p = pattern.split('%')
let r = replacement.split('%')
return {
pattern_prefix: p[0],
pattern_suffix: p.slice(1).join('%'),
replacement_prefix: r[0],
replacement_suffix: r.slice(1).join('%'),
}
}
if (process.argv.length !== 2+3) {
console.error('Usage: gmake-patsubst.js PATTERN REPLACEMENT TEXT')
process.exit(1)
}
let [pattern, replacement, text] = process.argv.slice(2)
let ps = affixes(pattern, replacement)
//console.log(ps)
console.log(text
.trim()
.split(/\s+/)
.map( (chunk) => transform(ps, pattern, replacement, chunk) )
.join(' '))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment