/*** This file is part of systemd. Copyright 2010 Lennart Poettering systemd is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. systemd 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with systemd; If not, see . ***/ #include #include #include #include #include #include #include #include #include #include #include "alloc-util.h" #include "ctype.h" #include "escape.h" #include "fd-util.h" #include "fileio.h" #include "fs-util.h" #include "hexdecoct.h" //#include "log.h" //#include "macro.h" #include "parse-util.h" #include "path-util.h" #include "random-util.h" #include "stdio-util.h" #include "string-util.h" #include "strv.h" //#include "time-util.h" #include "umask-util.h" #include "utf8.h" int write_string_stream(FILE *f, const char *line, bool enforce_newline) { assert(f); assert(line); fputs(line, f); if (enforce_newline && !endswith(line, "\n")) fputc('\n', f); return fflush_and_check(f); } static int write_string_file_atomic(const char *fn, const char *line, bool enforce_newline) { _cleanup_fclose_ FILE *f = NULL; _cleanup_free_ char *p = NULL; int r; assert(fn); assert(line); r = fopen_temporary(fn, &f, &p); if (r < 0) return r; (void) fchmod_umask(fileno(f), 0644); r = write_string_stream(f, line, enforce_newline); if (r >= 0) { if (rename(p, fn) < 0) r = -errno; } if (r < 0) (void) unlink(p); return r; } int write_string_file(const char *fn, const char *line, WriteStringFileFlags flags) { _cleanup_fclose_ FILE *f = NULL; int q, r; assert(fn); assert(line); if (flags & WRITE_STRING_FILE_ATOMIC) { assert(flags & WRITE_STRING_FILE_CREATE); r = write_string_file_atomic(fn, line, !(flags & WRITE_STRING_FILE_AVOID_NEWLINE)); if (r < 0) goto fail; return r; } if (flags & WRITE_STRING_FILE_CREATE) { f = fopen(fn, "we"); if (!f) { r = -errno; goto fail; } } else { int fd; /* We manually build our own version of fopen(..., "we") that * works without O_CREAT */ fd = open(fn, O_WRONLY|O_CLOEXEC|O_NOCTTY); if (fd < 0) { r = -errno; goto fail; } f = fdopen(fd, "we"); if (!f) { r = -errno; safe_close(fd); goto fail; } } r = write_string_stream(f, line, !(flags & WRITE_STRING_FILE_AVOID_NEWLINE)); if (r < 0) goto fail; return 0; fail: if (!(flags & WRITE_STRING_FILE_VERIFY_ON_FAILURE)) return r; f = safe_fclose(f); /* OK, the operation failed, but let's see if the right * contents in place already. If so, eat up the error. */ q = verify_file(fn, line, !(flags & WRITE_STRING_FILE_AVOID_NEWLINE)); if (q <= 0) return r; return 0; } int read_one_line_file(const char *fn, char **line) { _cleanup_fclose_ FILE *f = NULL; char t[LINE_MAX], *c; assert(fn); assert(line); f = fopen(fn, "re"); if (!f) return -errno; if (!fgets(t, sizeof(t), f)) { if (ferror(f)) return errno > 0 ? -errno : -EIO; t[0] = 0; } c = strdup(t); if (!c) return -ENOMEM; truncate_nl(c); *line = c; return 0; } int verify_file(const char *fn, const char *blob, bool accept_extra_nl) { _cleanup_fclose_ FILE *f = NULL; _cleanup_free_ char *buf = NULL; size_t l, k; assert(fn); assert(blob); l = strlen(blob); if (accept_extra_nl && endswith(blob, "\n")) accept_extra_nl = false; buf = malloc(l + accept_extra_nl + 1); if (!buf) return -ENOMEM; f = fopen(fn, "re"); if (!f) return -errno; /* We try to read one byte more than we need, so that we know whether we hit eof */ errno = 0; k = fread(buf, 1, l + accept_extra_nl + 1, f); if (ferror(f)) return errno > 0 ? -errno : -EIO; if (k != l && k != l + accept_extra_nl) return 0; if (memcmp(buf, blob, l) != 0) return 0; if (k > l && buf[l] != '\n') return 0; return 1; } int read_full_stream(FILE *f, char **contents, size_t *size) { size_t n, l; _cleanup_free_ char *buf = NULL; struct stat st; assert(f); assert(contents); if (fstat(fileno(f), &st) < 0) return -errno; n = LINE_MAX; if (S_ISREG(st.st_mode)) { /* Safety check */ if (st.st_size > 4*1024*1024) return -E2BIG; /* Start with the right file size, but be prepared for * files from /proc which generally report a file size * of 0 */ if (st.st_size > 0) n = st.st_size; } l = 0; for (;;) { char *t; size_t k; t = realloc(buf, n+1); if (!t) return -ENOMEM; buf = t; k = fread(buf + l, 1, n - l, f); if (k <= 0) { if (ferror(f)) return -errno; break; } l += k; n *= 2; /* Safety check */ if (n > 4*1024*1024) return -E2BIG; } buf[l] = 0; *contents = buf; buf = NULL; /* do not free */ if (size) *size = l; return 0; } int read_full_file(const char *fn, char **contents, size_t *size) { _cleanup_fclose_ FILE *f = NULL; assert(fn); assert(contents); f = fopen(fn, "re"); if (!f) return -errno; return read_full_stream(f, contents, size); } static int parse_env_file_internal( FILE *f, const char *fname, const char *newline, int (*push) (const char *filename, unsigned line, const char *key, char *value, void *userdata, int *n_pushed), void *userdata, int *n_pushed) { _cleanup_free_ char *contents = NULL, *key = NULL; size_t key_alloc = 0, n_key = 0, value_alloc = 0, n_value = 0, last_value_whitespace = (size_t) -1, last_key_whitespace = (size_t) -1; char *p, *value = NULL; int r; unsigned line = 1; enum { PRE_KEY, KEY, PRE_VALUE, VALUE, VALUE_ESCAPE, SINGLE_QUOTE_VALUE, SINGLE_QUOTE_VALUE_ESCAPE, DOUBLE_QUOTE_VALUE, DOUBLE_QUOTE_VALUE_ESCAPE, COMMENT, COMMENT_ESCAPE } state = PRE_KEY; assert(newline); if (f) r = read_full_stream(f, &contents, NULL); else r = read_full_file(fname, &contents, NULL); if (r < 0) return r; for (p = contents; *p; p++) { char c = *p; switch (state) { case PRE_KEY: if (strchr(COMMENTS, c)) state = COMMENT; else if (!strchr(WHITESPACE, c)) { state = KEY; last_key_whitespace = (size_t) -1; if (!GREEDY_REALLOC(key, key_alloc, n_key+2)) { r = -ENOMEM; goto fail; } key[n_key++] = c; } break; case KEY: if (strchr(newline, c)) { state = PRE_KEY; line++; n_key = 0; } else if (c == '=') { state = PRE_VALUE; last_value_whitespace = (size_t) -1; } else { if (!strchr(WHITESPACE, c)) last_key_whitespace = (size_t) -1; else if (last_key_whitespace == (size_t) -1) last_key_whitespace = n_key; if (!GREEDY_REALLOC(key, key_alloc, n_key+2)) { r = -ENOMEM; goto fail; } key[n_key++] = c; } break; case PRE_VALUE: if (strchr(newline, c)) { state = PRE_KEY; line++; key[n_key] = 0; if (value) value[n_value] = 0; /* strip trailing whitespace from key */ if (last_key_whitespace != (size_t) -1) key[last_key_whitespace] = 0; r = push(fname, line, key, value, userdata, n_pushed); if (r < 0) goto fail; n_key = 0; value = NULL; value_alloc = n_value = 0; } else if (c == '\'') state = SINGLE_QUOTE_VALUE; else if (c == '\"') state = DOUBLE_QUOTE_VALUE; else if (c == '\\') state = VALUE_ESCAPE; else if (!strchr(WHITESPACE, c)) { state = VALUE; if (!GREEDY_REALLOC(value, value_alloc, n_value+2)) { r = -ENOMEM; goto fail; } value[n_value++] = c; } break; case VALUE: if (strchr(newline, c)) { state = PRE_KEY; line++; key[n_key] = 0; if (value) value[n_value] = 0; /* Chomp off trailing whitespace from value */ if (last_value_whitespace != (size_t) -1) value[last_value_whitespace] = 0; /* strip trailing whitespace from key */ if (last_key_whitespace != (size_t) -1) key[last_key_whitespace] = 0; r = push(fname, line, key, value, userdata, n_pushed); if (r < 0) goto fail; n_key = 0; value = NULL; value_alloc = n_value = 0; } else if (c == '\\') { state = VALUE_ESCAPE; last_value_whitespace = (size_t) -1; } else { if (!strchr(WHITESPACE, c)) last_value_whitespace = (size_t) -1; else if (last_value_whitespace == (size_t) -1) last_value_whitespace = n_value; if (!GREEDY_REALLOC(value, value_alloc, n_value+2)) { r = -ENOMEM; goto fail; } value[n_value++] = c; } break; case VALUE_ESCAPE: state = VALUE; if (!strchr(newline, c)) { /* Escaped newlines we eat up entirely */ if (!GREEDY_REALLOC(value, value_alloc, n_value+2)) { r = -ENOMEM; goto fail; } value[n_value++] = c; } break; case SINGLE_QUOTE_VALUE: if (c == '\'') state = PRE_VALUE; else if (c == '\\') state = SINGLE_QUOTE_VALUE_ESCAPE; else { if (!GREEDY_REALLOC(value, value_alloc, n_value+2)) { r = -ENOMEM; goto fail; } value[n_value++] = c; } break; case SINGLE_QUOTE_VALUE_ESCAPE: state = SINGLE_QUOTE_VALUE; if (!strchr(newline, c)) { if (!GREEDY_REALLOC(value, value_alloc, n_value+2)) { r = -ENOMEM; goto fail; } value[n_value++] = c; } break; case DOUBLE_QUOTE_VALUE: if (c == '\"') state = PRE_VALUE; else if (c == '\\') state = DOUBLE_QUOTE_VALUE_ESCAPE; else { if (!GREEDY_REALLOC(value, value_alloc, n_value+2)) { r = -ENOMEM; goto fail; } value[n_value++] = c; } break; case DOUBLE_QUOTE_VALUE_ESCAPE: state = DOUBLE_QUOTE_VALUE; if (!strchr(newline, c)) { if (!GREEDY_REALLOC(value, value_alloc, n_value+2)) { r = -ENOMEM; goto fail; } value[n_value++] = c; } break; case COMMENT: if (c == '\\') state = COMMENT_ESCAPE; else if (strchr(newline, c)) { state = PRE_KEY; line++; } break; case COMMENT_ESCAPE: state = COMMENT; break; } } if (state == PRE_VALUE || state == VALUE || state == VALUE_ESCAPE || state == SINGLE_QUOTE_VALUE || state == SINGLE_QUOTE_VALUE_ESCAPE || state == DOUBLE_QUOTE_VALUE || state == DOUBLE_QUOTE_VALUE_ESCAPE) { key[n_key] = 0; if (value) value[n_value] = 0; if (state == VALUE) if (last_value_whitespace != (size_t) -1) value[last_value_whitespace] = 0; /* strip trailing whitespace from key */ if (last_key_whitespace != (size_t) -1) key[last_key_whitespace] = 0; r = push(fname, line, key, value, userdata, n_pushed); if (r < 0) goto fail; } return 0; fail: free(value); return r; } static int parse_env_file_push( const char *filename, unsigned line, const char *key, char *value, void *userdata, int *n_pushed) { const char *k; va_list aq, *ap = userdata; if (!utf8_is_valid(key)) { _cleanup_free_ char *p = NULL; p = utf8_escape_invalid(key); log_error("%s:%u: invalid UTF-8 in key '%s', ignoring.", strna(filename), line, p); return -EINVAL; } if (value && !utf8_is_valid(value)) { _cleanup_free_ char *p = NULL; p = utf8_escape_invalid(value); log_error("%s:%u: invalid UTF-8 value for key %s: '%s', ignoring.", strna(filename), line, key, p); return -EINVAL; } va_copy(aq, *ap); while ((k = va_arg(aq, const char *))) { char **v; v = va_arg(aq, char **); if (streq(key, k)) { va_end(aq); free(*v); *v = value; if (n_pushed) (*n_pushed)++; return 1; } } va_end(aq); free(value); return 0; } int parse_env_file( const char *fname, const char *newline, ...) { va_list ap; int r, n_pushed = 0; if (!newline) newline = NEWLINE; va_start(ap, newline); r = parse_env_file_internal(NULL, fname, newline, parse_env_file_push, &ap, &n_pushed); va_end(ap); return r < 0 ? r : n_pushed; } #if 0 /// UNNEEDED by elogind static int load_env_file_push( const char *filename, unsigned line, const char *key, char *value, void *userdata, int *n_pushed) { char ***m = userdata; char *p; int r; if (!utf8_is_valid(key)) { _cleanup_free_ char *t = utf8_escape_invalid(key); log_error("%s:%u: invalid UTF-8 for key '%s', ignoring.", strna(filename), line, t); return -EINVAL; } if (value && !utf8_is_valid(value)) { _cleanup_free_ char *t = utf8_escape_invalid(value); log_error("%s:%u: invalid UTF-8 value for key %s: '%s', ignoring.", strna(filename), line, key, t); return -EINVAL; } p = strjoin(key, "=", strempty(value), NULL); if (!p) return -ENOMEM; r = strv_consume(m, p); if (r < 0) return r; if (n_pushed) (*n_pushed)++; free(value); return 0; } int load_env_file(FILE *f, const char *fname, const char *newline, char ***rl) { char **m = NULL; int r; if (!newline) newline = NEWLINE; r = parse_env_file_internal(f, fname, newline, load_env_file_push, &m, NULL); if (r < 0) { strv_free(m); return r; } *rl = m; return 0; } static int load_env_file_push_pairs( const char *filename, unsigned line, const char *key, char *value, void *userdata, int *n_pushed) { char ***m = userdata; int r; if (!utf8_is_valid(key)) { _cleanup_free_ char *t = utf8_escape_invalid(key); log_error("%s:%u: invalid UTF-8 for key '%s', ignoring.", strna(filename), line, t); return -EINVAL; } if (value && !utf8_is_valid(value)) { _cleanup_free_ char *t = utf8_escape_invalid(value); log_error("%s:%u: invalid UTF-8 value for key %s: '%s', ignoring.", strna(filename), line, key, t); return -EINVAL; } r = strv_extend(m, key); if (r < 0) return -ENOMEM; if (!value) { r = strv_extend(m, ""); if (r < 0) return -ENOMEM; } else { r = strv_push(m, value); if (r < 0) return r; } if (n_pushed) (*n_pushed)++; return 0; } int load_env_file_pairs(FILE *f, const char *fname, const char *newline, char ***rl) { char **m = NULL; int r; if (!newline) newline = NEWLINE; r = parse_env_file_internal(f, fname, newline, load_env_file_push_pairs, &m, NULL); if (r < 0) { strv_free(m); return r; } *rl = m; return 0; } static void write_env_var(FILE *f, const char *v) { const char *p; p = strchr(v, '='); if (!p) { /* Fallback */ fputs(v, f); fputc('\n', f); return; } p++; fwrite(v, 1, p-v, f); if (string_has_cc(p, NULL) || chars_intersect(p, WHITESPACE SHELL_NEED_QUOTES)) { fputc('\"', f); for (; *p; p++) { if (strchr(SHELL_NEED_ESCAPE, *p)) fputc('\\', f); fputc(*p, f); } fputc('\"', f); } else fputs(p, f); fputc('\n', f); } int write_env_file(const char *fname, char **l) { _cleanup_fclose_ FILE *f = NULL; _cleanup_free_ char *p = NULL; char **i; int r; assert(fname); r = fopen_temporary(fname, &f, &p); if (r < 0) return r; fchmod_umask(fileno(f), 0644); STRV_FOREACH(i, l) write_env_var(f, *i); r = fflush_and_check(f); if (r >= 0) { if (rename(p, fname) >= 0) return 0; r = -errno; } unlink(p); return r; } int executable_is_script(const char *path, char **interpreter) { int r; _cleanup_free_ char *line = NULL; int len; char *ans; assert(path); r = read_one_line_file(path, &line); if (r < 0) return r; if (!startswith(line, "#!")) return 0; ans = strstrip(line + 2); len = strcspn(ans, " \t"); if (len == 0) return 0; ans = strndup(ans, len); if (!ans) return -ENOMEM; *interpreter = ans; return 1; } #endif // 0 /** * Retrieve one field from a file like /proc/self/status. pattern * should not include whitespace or the delimiter (':'). pattern matches only * the beginning of a line. Whitespace before ':' is skipped. Whitespace and * zeros after the ':' will be skipped. field must be freed afterwards. * terminator specifies the terminating characters of the field value (not * included in the value). */ int get_proc_field(const char *filename, const char *pattern, const char *terminator, char **field) { _cleanup_free_ char *status = NULL; char *t, *f; size_t len; int r; assert(terminator); assert(filename); assert(pattern); assert(field); r = read_full_file(filename, &status, NULL); if (r < 0) return r; t = status; do { bool pattern_ok; do { t = strstr(t, pattern); if (!t) return -ENOENT; /* Check that pattern occurs in beginning of line. */ pattern_ok = (t == status || t[-1] == '\n'); t += strlen(pattern); } while (!pattern_ok); t += strspn(t, " \t"); if (!*t) return -ENOENT; } while (*t != ':'); t++; if (*t) { t += strspn(t, " \t"); /* Also skip zeros, because when this is used for * capabilities, we don't want the zeros. This way the * same capability set always maps to the same string, * irrespective of the total capability set size. For * other numbers it shouldn't matter. */ t += strspn(t, "0"); /* Back off one char if there's nothing but whitespace and zeros */ if (!*t || isspace(*t)) t--; } len = strcspn(t, terminator); f = strndup(t, len); if (!f) return -ENOMEM; *field = f; return 0; } DIR *xopendirat(int fd, const char *name, int flags) { int nfd; DIR *d; assert(!(flags & O_CREAT)); nfd = openat(fd, name, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC|flags, 0); if (nfd < 0) return NULL; d = fdopendir(nfd); if (!d) { safe_close(nfd); return NULL; } return d; } #if 0 /// UNNEEDED by elogind static int search_and_fopen_internal(const char *path, const char *mode, const char *root, char **search, FILE **_f) { char **i; assert(path); assert(mode); assert(_f); if (!path_strv_resolve_uniq(search, root)) return -ENOMEM; STRV_FOREACH(i, search) { _cleanup_free_ char *p = NULL; FILE *f; if (root) p = strjoin(root, *i, "/", path, NULL); else p = strjoin(*i, "/", path, NULL); if (!p) return -ENOMEM; f = fopen(p, mode); if (f) { *_f = f; return 0; } if (errno != ENOENT) return -errno; } return -ENOENT; } int search_and_fopen(const char *path, const char *mode, const char *root, const char **search, FILE **_f) { _cleanup_strv_free_ char **copy = NULL; assert(path); assert(mode); assert(_f); if (path_is_absolute(path)) { FILE *f; f = fopen(path, mode); if (f) { *_f = f; return 0; } return -errno; } copy = strv_copy((char**) search); if (!copy) return -ENOMEM; return search_and_fopen_internal(path, mode, root, copy, _f); } int search_and_fopen_nulstr(const char *path, const char *mode, const char *root, const char *search, FILE **_f) { _cleanup_strv_free_ char **s = NULL; if (path_is_absolute(path)) { FILE *f; f = fopen(path, mode); if (f) { *_f = f; return 0; } return -errno; } s = strv_split_nulstr(search); if (!s) return -ENOMEM; return search_and_fopen_internal(path, mode, root, s, _f); } #endif // 0 int fopen_temporary(const char *path, FILE **_f, char **_temp_path) { FILE *f; char *t; int r, fd; assert(path); assert(_f); assert(_temp_path); r = tempfn_xxxxxx(path, NULL, &t); if (r < 0) return r; fd = mkostemp_safe(t, O_WRONLY|O_CLOEXEC); if (fd < 0) { free(t); return -errno; } f = fdopen(fd, "we"); if (!f) { unlink_noerrno(t); free(t); safe_close(fd); return -errno; } *_f = f; *_temp_path = t; return 0; } int fflush_and_check(FILE *f) { assert(f); errno = 0; fflush(f); if (ferror(f)) return errno > 0 ? -errno : -EIO; return 0; } /* This is much like like mkostemp() but is subject to umask(). */ int mkostemp_safe(char *pattern, int flags) { _cleanup_umask_ mode_t u = 0; int fd; assert(pattern); u = umask(077); fd = mkostemp(pattern, flags); if (fd < 0) return -errno; return fd; } #if 0 /// UNNEEDED by elogind int open_tmpfile(const char *path, int flags) { char *p; int fd; assert(path); #ifdef O_TMPFILE /* Try O_TMPFILE first, if it is supported */ fd = open(path, flags|O_TMPFILE|O_EXCL, S_IRUSR|S_IWUSR); if (fd >= 0) return fd; #endif /* Fall back to unguessable name + unlinking */ p = strjoina(path, "/systemd-tmp-XXXXXX"); fd = mkostemp_safe(p, flags); if (fd < 0) return fd; unlink(p); return fd; } #endif // 0 int tempfn_xxxxxx(const char *p, const char *extra, char **ret) { const char *fn; char *t; assert(p); assert(ret); /* * Turns this: * /foo/bar/waldo * * Into this: * /foo/bar/.#waldoXXXXXX */ fn = basename(p); if (!filename_is_valid(fn)) return -EINVAL; if (extra == NULL) extra = ""; t = new(char, strlen(p) + 2 + strlen(extra) + 6 + 1); if (!t) return -ENOMEM; strcpy(stpcpy(stpcpy(stpcpy(mempcpy(t, p, fn - p), ".#"), extra), fn), "XXXXXX"); *ret = path_kill_slashes(t); return 0; } #if 0 /// UNNEEDED by elogind int tempfn_random(const char *p, const char *extra, char **ret) { const char *fn; char *t, *x; uint64_t u; unsigned i; assert(p); assert(ret); /* * Turns this: * /foo/bar/waldo * * Into this: * /foo/bar/.#waldobaa2a261115984a9 */ fn = basename(p); if (!filename_is_valid(fn)) return -EINVAL; if (!extra) extra = ""; t = new(char, strlen(p) + 2 + strlen(extra) + 16 + 1); if (!t) return -ENOMEM; x = stpcpy(stpcpy(stpcpy(mempcpy(t, p, fn - p), ".#"), extra), fn); u = random_u64(); for (i = 0; i < 16; i++) { *(x++) = hexchar(u & 0xF); u >>= 4; } *x = 0; *ret = path_kill_slashes(t); return 0; } int tempfn_random_child(const char *p, const char *extra, char **ret) { char *t, *x; uint64_t u; unsigned i; assert(p); assert(ret); /* Turns this: * /foo/bar/waldo * Into this: * /foo/bar/waldo/.#3c2b6219aa75d7d0 */ if (!extra) extra = ""; t = new(char, strlen(p) + 3 + strlen(extra) + 16 + 1); if (!t) return -ENOMEM; x = stpcpy(stpcpy(stpcpy(t, p), "/.#"), extra); u = random_u64(); for (i = 0; i < 16; i++) { *(x++) = hexchar(u & 0xF); u >>= 4; } *x = 0; *ret = path_kill_slashes(t); return 0; } int write_timestamp_file_atomic(const char *fn, usec_t n) { char ln[DECIMAL_STR_MAX(n)+2]; /* Creates a "timestamp" file, that contains nothing but a * usec_t timestamp, formatted in ASCII. */ if (n <= 0 || n >= USEC_INFINITY) return -ERANGE; xsprintf(ln, USEC_FMT "\n", n); return write_string_file(fn, ln, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); } int read_timestamp_file(const char *fn, usec_t *ret) { _cleanup_free_ char *ln = NULL; uint64_t t; int r; r = read_one_line_file(fn, &ln); if (r < 0) return r; r = safe_atou64(ln, &t); if (r < 0) return r; if (t <= 0 || t >= (uint64_t) USEC_INFINITY) return -ERANGE; *ret = (usec_t) t; return 0; } int fputs_with_space(FILE *f, const char *s, const char *separator, bool *space) { int r; assert(s); /* Outputs the specified string with fputs(), but optionally prefixes it with a separator. The *space parameter * when specified shall initially point to a boolean variable initialized to false. It is set to true after the * first invocation. This call is supposed to be use in loops, where a separator shall be inserted between each * element, but not before the first one. */ if (!f) f = stdout; if (space) { if (!separator) separator = " "; if (*space) { r = fputs(separator, f); if (r < 0) return r; } *space = true; } return fputs(s, f); } #endif // 0