/*
* Copyright (C) 2002 Andrew Tridgell
- * Copyright (C) 2009-2013 Joel Rosdahl
+ * Copyright (C) 2009-2014 Joel Rosdahl
*
* This program 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
static bool
init_log(void)
{
- extern char *cache_logfile;
+ extern struct conf *conf;
if (logfile) {
return true;
}
- if (!cache_logfile) {
+ assert(conf);
+ if (str_eq(conf->log_file, "")) {
return false;
}
- logfile = fopen(cache_logfile, "a");
+ logfile = fopen(conf->log_file, "a");
if (logfile) {
+#ifndef _WIN32
int fd = fileno(logfile);
int flags = fcntl(fd, F_GETFD, 0);
if (flags >= 0) {
fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
}
+#endif
return true;
} else {
return false;
}
static void
-log_prefix(void)
+log_prefix(bool log_updated_time)
{
#ifdef HAVE_GETTIMEOFDAY
char timestamp[100];
struct timeval tv;
struct tm *tm;
+ static char prefix[200];
- gettimeofday(&tv, NULL);
+ if (log_updated_time) {
+ gettimeofday(&tv, NULL);
#ifdef __MINGW64_VERSION_MAJOR
- tm = _localtime32(&tv.tv_sec);
+ tm = localtime((time_t*)&tv.tv_sec);
#else
- tm = localtime(&tv.tv_sec);
+ tm = localtime(&tv.tv_sec);
#endif
- strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", tm);
- fprintf(logfile, "[%s.%06d %-5d] ", timestamp, (int)tv.tv_usec,
- (int)getpid());
+ strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", tm);
+ snprintf(prefix, sizeof(prefix),
+ "[%s.%06d %-5d] ", timestamp, (int)tv.tv_usec, (int)getpid());
+ }
+ fputs(prefix, logfile);
#else
fprintf(logfile, "[%-5d] ", (int)getpid());
#endif
}
+static long
+path_max(const char *path)
+{
+#ifdef PATH_MAX
+ (void)path;
+ return PATH_MAX;
+#elif defined(MAXPATHLEN)
+ (void)path;
+ return MAXPATHLEN;
+#elif defined(_PC_PATH_MAX)
+ long maxlen = pathconf(path, _PC_PATH_MAX);
+ if (maxlen >= 4096) {
+ return maxlen;
+ } else {
+ return 4096;
+ }
+#endif
+}
+
+static void
+vlog(const char *format, va_list ap, bool log_updated_time)
+{
+ if (!init_log()) {
+ return;
+ }
+
+ log_prefix(log_updated_time);
+ vfprintf(logfile, format, ap);
+ fprintf(logfile, "\n");
+}
+
/*
- * Write a message to the CCACHE_LOGFILE location (adding a newline).
+ * Write a message to the log file (adding a newline) and flush.
*/
void
cc_log(const char *format, ...)
{
va_list ap;
-
- if (!init_log()) {
- return;
+ va_start(ap, format);
+ vlog(format, ap, true);
+ va_end(ap);
+ if (logfile) {
+ fflush(logfile);
}
+}
- log_prefix();
+/*
+ * Write a message to the log file (adding a newline) without flushing and with
+ * a reused timestamp.
+ */
+void
+cc_bulklog(const char *format, ...)
+{
+ va_list ap;
va_start(ap, format);
- vfprintf(logfile, format, ap);
+ vlog(format, ap, false);
va_end(ap);
- fprintf(logfile, "\n");
- fflush(logfile);
}
/*
return;
}
- log_prefix();
+ log_prefix(true);
fputs(prefix, logfile);
print_command(logfile, argv);
fflush(logfile);
va_end(ap);
cc_log("FATAL: %s", msg);
- fprintf(stderr, "ccache: FATAL: %s\n", msg);
+ fprintf(stderr, "ccache: error: %s\n", msg);
exit(1);
}
#endif
/*
- * Copy src to dest, decompressing src if needed. compress_dest decides whether
- * dest will be compressed.
+ * Copy src to dest, decompressing src if needed. compress_level > 0 decides
+ * whether dest will be compressed, and with which compression level.
*/
int
-copy_file(const char *src, const char *dest, int compress_dest)
+copy_file(const char *src, const char *dest, int compress_level)
{
- int fd_in = -1, fd_out = -1;
+ int fd_in, fd_out;
gzFile gz_in = NULL, gz_out = NULL;
char buf[10240];
int n, written;
struct stat st;
int errnum;
- tmp_name = format("%s.%s.XXXXXX", dest, tmp_string());
- cc_log("Copying %s to %s via %s (%s)",
- src, dest, tmp_name, compress_dest ? "compressed": "uncompressed");
+ /* open destination file */
+ tmp_name = x_strdup(dest);
+ fd_out = create_tmp_fd(&tmp_name);
+ cc_log("Copying %s to %s via %s (%scompressed)",
+ src, dest, tmp_name, compress_level > 0 ? "" : "un");
/* open source file */
fd_in = open(src, O_RDONLY | O_BINARY);
if (fd_in == -1) {
cc_log("open error: %s", strerror(errno));
- return -1;
+ goto error;
}
gz_in = gzdopen(fd_in, "rb");
if (!gz_in) {
cc_log("gzdopen(src) error: %s", strerror(errno));
close(fd_in);
- return -1;
- }
-
- /* open destination file */
- fd_out = mkstemp(tmp_name);
- if (fd_out == -1) {
- cc_log("mkstemp error: %s", strerror(errno));
goto error;
}
- if (compress_dest) {
+ if (compress_level > 0) {
/*
* A gzip file occupies at least 20 bytes, so it will always
* occupy an entire filesystem block, even for empty files.
goto error;
}
if (file_size(&st) == 0) {
- compress_dest = 0;
+ compress_level = 0;
}
}
- if (compress_dest) {
+ if (compress_level > 0) {
gz_out = gzdopen(dup(fd_out), "wb");
if (!gz_out) {
cc_log("gzdopen(dest) error: %s", strerror(errno));
goto error;
}
+ gzsetparams(gz_out, compress_level, Z_DEFAULT_STRATEGY);
}
while ((n = gzread(gz_in, buf, sizeof(buf))) > 0) {
- if (compress_dest) {
+ if (compress_level > 0) {
written = gzwrite(gz_out, buf, n);
} else {
ssize_t count;
} while (written < n);
}
if (written != n) {
- if (compress_dest) {
+ if (compress_level > 0) {
cc_log("gzwrite error: %s (errno: %s)",
gzerror(gz_in, &errnum),
strerror(errno));
/* Run copy_file() and, if successful, delete the source file. */
int
-move_file(const char *src, const char *dest, int compress_dest)
+move_file(const char *src, const char *dest, int compress_level)
{
int ret;
- ret = copy_file(src, dest, compress_dest);
+ ret = copy_file(src, dest, compress_level);
if (ret != -1) {
x_unlink(src);
}
* are on the same file system.
*/
int
-move_uncompressed_file(const char *src, const char *dest, int compress_dest)
+move_uncompressed_file(const char *src, const char *dest, int compress_level)
{
- if (compress_dest) {
- return move_file(src, dest, compress_dest);
+ if (compress_level > 0) {
+ return move_file(src, dest, compress_level);
} else {
return x_rename(src, dest);
}
return 0;
}
+/* Create directories leading to path. Returns 0 on success, otherwise -1. */
+int
+create_parent_dirs(const char *path)
+{
+ struct stat st;
+ int res;
+ char *parent = dirname(path);
+
+ if (stat(parent, &st) == 0) {
+ if (S_ISDIR(st.st_mode)) {
+ res = 0;
+ } else {
+ res = -1;
+ errno = ENOTDIR;
+ }
+ } else {
+ res = create_parent_dirs(parent);
+ if (res == 0) {
+ res = mkdir(parent, 0777);
+ /* Have to handle the condition of the directory already existing because
+ * the file system could have changed in between calling stat and
+ * actually creating the directory. This can happen when there are
+ * multiple instances of ccache running and trying to create the same
+ * directory chain, which usually is the case when the cache root does
+ * not initially exist. As long as one of the processes creates the
+ * directories then our condition is satisfied and we avoid a race
+ * condition.
+ */
+ if (res != 0 && errno == EEXIST) {
+ res = 0;
+ }
+ } else {
+ res = -1;
+ }
+ }
+ free(parent);
+ return res;
+}
+
/*
* Return a static string with the current hostname.
*/
}
/*
- * Return a string to be used to distinguish temporary files. Also tries to
- * cope with NFS by adding the local hostname.
+ * Return a string to be passed to mkstemp to create a temporary file. Also
+ * tries to cope with NFS by adding the local hostname.
*/
const char *
tmp_string(void)
static char *ret;
if (!ret) {
- ret = format("%s.%u", get_hostname(), (unsigned)getpid());
+ ret = format("%s.%u.XXXXXX", get_hostname(), (unsigned)getpid());
}
return ret;
}
-/* Return the hash result as a hex string. Caller frees. */
+/*
+ * Return the hash result as a hex string. Size -1 means don't include size
+ * suffix. Caller frees.
+ */
char *
-format_hash_as_string(const unsigned char *hash, unsigned size)
+format_hash_as_string(const unsigned char *hash, int size)
{
char *ret;
int i;
for (i = 0; i < 16; i++) {
sprintf(&ret[i*2], "%02x", (unsigned) hash[i]);
}
- sprintf(&ret[i*2], "-%u", size);
+ if (size >= 0) {
+ sprintf(&ret[i*2], "-%u", size);
+ }
return ret;
}
goto error;
}
f = fopen(filename, "w");
- if (!f) goto error;
+ if (!f) {
+ goto error;
+ }
if (fwrite(CACHEDIR_TAG, sizeof(CACHEDIR_TAG)-1, 1, f) != 1) {
fclose(f);
goto error;
}
- if (fclose(f)) goto error;
+ if (fclose(f)) {
+ goto error;
+ }
success:
free(filename);
return 0;
}
va_end(ap);
- if (!*ptr) fatal("Internal error in format");
+ if (!*ptr) {
+ fatal("Internal error in format");
+ }
return ptr;
}
x_realloc(void *ptr, size_t size)
{
void *p2;
- if (!ptr) return x_malloc(size);
+ if (!ptr) {
+ return x_malloc(size);
+ }
p2 = realloc(ptr, size);
if (!p2) {
fatal("x_realloc: Could not allocate %lu bytes", (unsigned long)size);
return p2;
}
+/* This is like unsetenv. */
+void x_unsetenv(const char *name)
+{
+#ifdef HAVE_UNSETENV
+ unsetenv(name);
+#else
+ putenv(x_strdup(name)); /* Leak to environment. */
+#endif
+}
/*
- * This is like x_asprintf() but frees *ptr if *ptr != NULL.
+ * Construct a string according to the format and store it in *ptr. The
+ * original *ptr is then freed.
*/
void
-x_asprintf2(char **ptr, const char *format, ...)
+reformat(char **ptr, const char *format, ...)
{
char *saved = *ptr;
va_list ap;
*ptr = NULL;
va_start(ap, format);
if (vasprintf(ptr, format, ap) == -1) {
- fatal("Out of memory in x_asprintf2");
+ fatal("Out of memory in reformat");
}
va_end(ap);
- if (!ptr) fatal("Out of memory in x_asprintf2");
+ if (!ptr) {
+ fatal("Out of memory in reformat");
+ }
if (saved) {
free(saved);
}
struct dirent *de;
d = opendir(dir);
- if (!d) return;
+ if (!d) {
+ return;
+ }
while ((de = readdir(d))) {
char *fname;
struct stat st;
- if (str_eq(de->d_name, ".")) continue;
- if (str_eq(de->d_name, "..")) continue;
+ if (str_eq(de->d_name, ".")) {
+ continue;
+ }
+ if (str_eq(de->d_name, "..")) {
+ continue;
+ }
- if (strlen(de->d_name) == 0) continue;
+ if (strlen(de->d_name) == 0) {
+ continue;
+ }
fname = format("%s/%s", dir, de->d_name);
if (lstat(fname, &st)) {
/* return the base name of a file - caller frees */
char *
-basename(const char *s)
+basename(const char *path)
{
char *p;
- p = strrchr(s, '/');
- if (p) s = p + 1;
+ p = strrchr(path, '/');
+ if (p) {
+ path = p + 1;
+ }
#ifdef _WIN32
- p = strrchr(s, '\\');
- if (p) s = p + 1;
+ p = strrchr(path, '\\');
+ if (p) {
+ path = p + 1;
+ }
#endif
- return x_strdup(s);
+ return x_strdup(path);
}
/* return the dir name of a file - caller frees */
char *
-dirname(char *s)
+dirname(const char *path)
{
char *p;
- char *p2 = NULL;
- s = x_strdup(s);
+#ifdef _WIN32
+ char *p2;
+#endif
+ char *s;
+ s = x_strdup(path);
p = strrchr(s, '/');
#ifdef _WIN32
p2 = strrchr(s, '\\');
-#endif
- if (p < p2)
+ if (!p || (p2 && p < p2)) {
p = p2;
- if (p) {
- *p = 0;
- return s;
- } else {
+ }
+#endif
+ if (!p) {
free(s);
- return x_strdup(".");
+ s = x_strdup(".");
+ } else if (p == s) {
+ *(p + 1) = 0;
+ } else {
+ *p = 0;
}
+ return s;
}
/*
#endif
}
-/* a safe open/create for read-write */
-int
-safe_open(const char *fname)
+/* Format a size as a human-readable string. Caller frees. */
+char *
+format_human_readable_size(uint64_t v)
{
- int fd = open(fname, O_RDWR|O_BINARY);
- if (fd == -1 && errno == ENOENT) {
- fd = open(fname, O_RDWR|O_CREAT|O_EXCL|O_BINARY, 0666);
- if (fd == -1 && errno == EEXIST) {
- fd = open(fname, O_RDWR|O_BINARY);
- }
+ char *s;
+ if (v >= 1000*1000*1000) {
+ s = format("%.1f GB", v/((double)(1000*1000*1000)));
+ } else if (v >= 1000*1000) {
+ s = format("%.1f MB", v/((double)(1000*1000)));
+ } else {
+ s = format("%.1f kB", v/((double)(1000)));
}
- return fd;
+ return s;
}
-/* Format a size (in KiB) as a human-readable string. Caller frees. */
+/* Format a size as a parsable string. Caller frees. */
char *
-format_size(size_t v)
+format_parsable_size_with_suffix(uint64_t size)
{
char *s;
- if (v >= 1024*1024) {
- s = format("%.1f Gbytes", v/((double)(1024*1024)));
- } else if (v >= 1024) {
- s = format("%.1f Mbytes", v/((double)(1024)));
+ if (size >= 1000*1000*1000) {
+ s = format("%.1fG", size / ((double)(1000*1000*1000)));
+ } else if (size >= 1000*1000) {
+ s = format("%.1fM", size / ((double)(1000*1000)));
+ } else if (size >= 1000) {
+ s = format("%.1fk", size / ((double)(1000)));
} else {
- s = format("%.0f Kbytes", (double)v);
+ s = format("%u", (unsigned)size);
}
return s;
}
-/* return a value in multiples of 1024 give a string that can end
- in K, M or G
-*/
-size_t
-value_units(const char *s)
+/*
+ * Parse a "size value", i.e. a string that can end in k, M, G, T (10-based
+ * suffixes) or Ki, Mi, Gi, Ti (2-based suffixes). For backward compatibility,
+ * K is also recognized as a synonym of k.
+ */
+bool
+parse_size_with_suffix(const char *str, uint64_t *size)
{
- char m;
- double v = atof(s);
- m = s[strlen(s)-1];
- switch (m) {
- case 'G':
- case 'g':
- default:
- v *= 1024*1024;
- break;
- case 'M':
- case 'm':
- v *= 1024;
- break;
- case 'K':
- case 'k':
- v *= 1;
- break;
- }
- return (size_t)v;
-}
+ char *p;
+ double x;
-#ifndef _WIN32
-static long
-path_max(const char *path)
-{
-#ifdef PATH_MAX
- (void)path;
- return PATH_MAX;
-#elif defined(MAXPATHLEN)
- (void)path;
- return MAXPATHLEN;
-#elif defined(_PC_PATH_MAX)
- long maxlen = pathconf(path, _PC_PATH_MAX);
- if (maxlen >= 4096) {
- return maxlen;
- } else {
- return 4096;
+ errno = 0;
+ x = strtod(str, &p);
+ if (errno != 0 || x < 0 || p == str || *str == '\0') {
+ return false;
}
-#endif
+
+ while (isspace(*p)) {
+ ++p;
+ }
+
+ if (*p != '\0') {
+ unsigned multiplier;
+ if (*(p+1) == 'i') {
+ multiplier = 1024;
+ } else {
+ multiplier = 1000;
+ }
+ switch (*p) {
+ case 'T':
+ x *= multiplier;
+ case 'G':
+ x *= multiplier;
+ case 'M':
+ x *= multiplier;
+ case 'K':
+ case 'k':
+ x *= multiplier;
+ break;
+ default:
+ return false;
+ }
+ }
+ *size = x;
+ return true;
}
+
/*
a sane realpath() function, trying to cope with stupid path limits and
a broken API
{
long maxlen = path_max(path);
char *ret, *p;
+#ifdef _WIN32
+ HANDLE path_handle;
+#endif
ret = x_malloc(maxlen);
#if HAVE_REALPATH
p = realpath(path, ret);
+#elif defined(_WIN32)
+ path_handle = CreateFile(
+ path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL, NULL);
+ GetFinalPathNameByHandle(path_handle, ret, maxlen, FILE_NAME_NORMALIZED);
+ CloseHandle(path_handle);
+ p = ret+4;// strip the \\?\ from the file name
#else
/* yes, there are such systems. This replacement relies on
the fact that when we call x_realpath we only care about symlinks */
free(ret);
return NULL;
}
-#endif /* !_WIN32 */
/* a getcwd that will returns an allocated buffer */
char *
{
unsigned size = 128;
- while (1) {
+ while (true) {
char *buffer = (char *)x_malloc(size);
if (getcwd(buffer, size) == buffer) {
return buffer;
}
}
-/* create an empty file */
-int
-create_empty_file(const char *fname)
+#ifndef HAVE_STRTOK_R
+/* strtok_r replacement */
+char *
+strtok_r(char *str, const char *delim, char **saveptr)
{
- int fd;
+ int len;
+ char *ret;
+ if (!str)
+ str = *saveptr;
+ len = strlen(str);
+ ret = strtok(str, delim);
+ if (ret) {
+ char *save = ret;
+ while (*save++);
+ if ((len + 1) == (intptr_t) (save - str))
+ save--;
+ *saveptr = save;
+ }
+ return ret;
+}
+#endif
- fd = open(fname, O_WRONLY|O_CREAT|O_TRUNC|O_EXCL|O_BINARY, 0666);
+/*
+ * Create an empty temporary file. *fname will be reallocated and set to the
+ * resulting filename. Returns an open file descriptor to the file.
+ */
+int
+create_tmp_fd(char **fname)
+{
+ char *template = format("%s.%s", *fname, tmp_string());
+ int fd = mkstemp(template);
+ if (fd == -1 && errno == ENOENT) {
+ if (create_parent_dirs(template) != 0) {
+ fatal("Failed to create directory %s: %s",
+ dirname(template), strerror(errno));
+ }
+ reformat(&template, "%s.%s", *fname, tmp_string());
+ fd = mkstemp(template);
+ }
if (fd == -1) {
- return -1;
+ fatal("Failed to create file %s: %s", template, strerror(errno));
}
- close(fd);
- return 0;
+ free(*fname);
+ *fname = template;
+ return fd;
+}
+
+/*
+ * Create an empty temporary file. *fname will be reallocated and set to the
+ * resulting filename. Returns an open FILE*.
+ */
+FILE *
+create_tmp_file(char **fname, const char *mode)
+{
+ FILE *file = fdopen(create_tmp_fd(fname), mode);
+ if (!file) {
+ fatal("Failed to create file %s: %s", *fname, strerror(errno));
+ }
+ return file;
+}
+
+/*
+ * Create an empty temporary file. *fname will be reallocated and set to the
+ * resulting filename.
+ */
+void
+create_empty_tmp_file(char **fname)
+{
+ close(create_tmp_fd(fname));
}
/*
if (p) {
return p;
}
+#ifdef _WIN32
+ p = getenv("APPDATA");
+ if (p) {
+ return p;
+ }
+#endif
#ifdef HAVE_GETPWUID
{
struct passwd *pwd = getpwuid(getuid());
++p1;
++p2;
}
- if (*p2 == '/') {
- /* s2 starts with "s1/". */
- return p1 - s1;
- }
- if (!*p2) {
- /* s2 is equal to s1. */
- if (p2 == s2 + 1) {
- /* Special case for s1 and s2 both being "/". */
- return 0;
- } else {
- return p1 - s1;
- }
- }
- /* Compute the common directory prefix */
- while (p1 > s1 && *p1 != '/') {
+ while ((*p1 && *p1 != '/') || (*p2 && *p2 != '/')) {
p1--;
p2--;
}
+ if (!*p1 && !*p2 && p2 == s2 + 1) {
+ /* Special case for s1 and s2 both being "/". */
+ return 0;
+ }
return p1 - s1;
}
if (common_prefix_len > 0 || !str_eq(from, "/")) {
for (p = from + common_prefix_len; *p; p++) {
if (*p == '/') {
- x_asprintf2(&result, "../%s", result);
+ reformat(&result, "../%s", result);
}
}
}
if (strlen(to) > common_prefix_len) {
- x_asprintf2(&result, "%s%s", result, to + common_prefix_len + 1);
+ reformat(&result, "%s%s", result, to + common_prefix_len + 1);
}
i = strlen(result) - 1;
while (i >= 0 && result[i] == '/') {
is_full_path(const char *path)
{
if (strchr(path, '/'))
- return 1;
+ return true;
#ifdef _WIN32
if (strchr(path, '\\'))
- return 1;
+ return true;
#endif
- return 0;
+ return false;
}
/*
int
tmp_unlink(const char *path)
{
- cc_log("Unlink %s (as-tmp)", path);
+ cc_log("Unlink %s", path);
return unlink(path);
}
* file. We don't care if the temp file is trashed, so it's always safe to
* unlink it first.
*/
- char *tmp_name = format("%s.tmp.rm.%s", path, tmp_string());
+ char *tmp_name = format("%s.rm.%s", path, tmp_string());
int result = 0;
cc_log("Unlink %s via %s", path, tmp_name);
if (x_rename(path, tmp_name) == -1) {
goto out;
}
if (unlink(tmp_name) == -1) {
- result = -1;
+ /* If it was released in a race, that's OK. */
+ if (errno != ENOENT) {
+ result = -1;
+ }
}
out:
free(tmp_name);
long maxlen = path_max(path);
ssize_t len;
char *buf;
-#ifdef PATH_MAX
- maxlen = PATH_MAX;
-#elif defined(MAXPATHLEN)
- maxlen = MAXPATHLEN;
-#elif defined(_PC_PATH_MAX)
- maxlen = pathconf(path, _PC_PATH_MAX);
-#endif
- if (maxlen < 4096) maxlen = 4096;
buf = x_malloc(maxlen);
len = readlink(path, buf, maxlen-1);
}
size_hint = (size_hint < 1024) ? 1024 : size_hint;
- fd = open(path, O_RDONLY);
+ fd = open(path, O_RDONLY | O_BINARY);
if (fd == -1) {
return false;
}
allocated = size_hint;
*data = x_malloc(allocated);
- ret = 0;
while (true) {
if (pos > allocated / 2) {
allocated *= 2;
/*
* Return the content (with NUL termination) of a text file, or NULL on error.
- * Caller frees.
+ * Caller frees. Size hint 0 means no hint.
*/
char *
-read_text_file(const char *path)
+read_text_file(const char *path, size_t size_hint)
{
size_t size;
char *data;
- if (read_file(path, 0, &data, &size)) {
+ if (read_file(path, size_hint, &data, &size)) {
data = x_realloc(data, size + 1);
data[size] = '\0';
return data;
return NULL;
}
}
+
+static bool
+expand_variable(const char **str, char **result, char **errmsg)
+{
+ bool curly;
+ const char *p, *q;
+ char *name;
+ const char *value;
+
+ assert(**str == '$');
+ p = *str + 1;
+ if (*p == '{') {
+ curly = true;
+ ++p;
+ } else {
+ curly = false;
+ }
+ q = p;
+ while (isalnum(*q) || *q == '_') {
+ ++q;
+ }
+ if (curly) {
+ if (*q != '}') {
+ *errmsg = format("syntax error: missing '}' after \"%s\"", p);
+ return NULL;
+ }
+ }
+
+ if (q == p) {
+ /* Special case: don't consider a single $ the start of a variable. */
+ reformat(result, "%s$", *result);
+ return true;
+ }
+
+ name = x_strndup(p, q - p);
+ value = getenv(name);
+ if (!value) {
+ *errmsg = format("environment variable \"%s\" not set", name);
+ free(name);
+ return false;
+ }
+ reformat(result, "%s%s", *result, value);
+ if (!curly) {
+ --q;
+ }
+ *str = q;
+ free(name);
+ return true;
+}
+
+/*
+ * Substitute all instances of $VAR or ${VAR}, where VAR is an environment
+ * variable, in a string. Caller frees. If one of the environment variables
+ * doesn't exist, NULL will be returned and *errmsg will be an appropriate
+ * error message (caller frees).
+ */
+char *
+subst_env_in_string(const char *str, char **errmsg)
+{
+ const char *p; /* Interval start. */
+ const char *q; /* Interval end. */
+ char *result;
+
+ assert(errmsg);
+ *errmsg = NULL;
+
+ result = x_strdup("");
+ p = str;
+ q = str;
+ for (q = str; *q; ++q) {
+ if (*q == '$') {
+ reformat(&result, "%s%.*s", result, (int)(q - p), p);
+ if (!expand_variable(&q, &result, errmsg)) {
+ free(result);
+ return NULL;
+ }
+ p = q + 1;
+ }
+ }
+ reformat(&result, "%s%.*s", result, (int)(q - p), p);
+ return result;
+}