Something went wrong. Try again.
A breadth-first version of the UNIX find command tavianator.com/projects/bfs.html
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283// Copyright © Tavian Barnes <tavianator@tavianator.com>// SPDX-License-Identifier: 0BSD
#include "bfstd.h"
#include "bfs.h"#include "bit.h"#include "diag.h"#include "sanity.h"#include "thread.h"#include "xregex.h"
#include <errno.h>#include <fcntl.h>#include <langinfo.h>#include <limits.h>#include <locale.h>#include <nl_types.h>#include <pthread.h>#include <sched.h>#include <stddef.h>#include <stdint.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/ioctl.h>#include <sys/resource.h>#include <sys/stat.h>#include <sys/types.h>#include <sys/wait.h>#include <termios.h>#include <unistd.h>#include <wchar.h>
#if __has_include(<sys/sysmacros.h>)# include <sys/sysmacros.h>#elif __has_include(<sys/mkdev.h>)# include <sys/mkdev.h>#endif
#if __has_include(<util.h>)# include <util.h>#endif
bool error_is_like(int error, int category) { if (error == category) { return true; }
switch (category) { case ENOENT: return error == ENOTDIR;
case ENOSYS: // https://github.com/opencontainers/runc/issues/2151 return error == EPERM;
#if __DragonFly__ // https://twitter.com/tavianator/status/1742991411203485713 case ENAMETOOLONG: return error == EFAULT;#endif }
return false;}
bool errno_is_like(int category) { return error_is_like(errno, category);}
int try(int ret) { if (ret >= 0) { return ret; } else { bfs_assert(errno > 0, "errno should be positive, was %d\n", errno); return -errno; }}
char *xdirname(const char *path) { size_t i = xbaseoff(path);
// Skip trailing slashes while (i > 0 && path[i - 1] == '/') { --i; }
if (i > 0) { return strndup(path, i); } else if (path[i] == '/') { return strdup("/"); } else { return strdup("."); }}
char *xbasename(const char *path) { size_t i = xbaseoff(path); size_t len = strcspn(path + i, "/"); if (len > 0) { return strndup(path + i, len); } else if (path[i] == '/') { return strdup("/"); } else { return strdup("."); }}
size_t xbaseoff(const char *path) { size_t i = strlen(path);
// Skip trailing slashes while (i > 0 && path[i - 1] == '/') { --i; }
// Find the beginning of the name while (i > 0 && path[i - 1] != '/') { --i; }
// Skip leading slashes while (path[i] == '/' && path[i + 1]) { ++i; }
return i;}
FILE *xfopen(const char *path, int flags) { char mode[4];
switch (flags & O_ACCMODE) { case O_RDONLY: strcpy(mode, "rb"); break; case O_WRONLY: strcpy(mode, "wb"); break; case O_RDWR: strcpy(mode, "r+b"); break; default: bfs_bug("Invalid access mode"); errno = EINVAL; return NULL; }
if (flags & O_APPEND) { mode[0] = 'a'; }
int fd; if (flags & O_CREAT) { fd = open(path, flags, 0666); } else { fd = open(path, flags); }
if (fd < 0) { return NULL; }
FILE *ret = fdopen(fd, mode); if (!ret) { close_quietly(fd); return NULL; }
return ret;}
char *xgetdelim(FILE *file, char delim) { char *chunk = NULL; size_t n = 0; ssize_t len = getdelim(&chunk, &n, delim, file); if (len > 0) { if (chunk[len - 1] == delim) { chunk[len - 1] = '\0'; } return chunk; } else { free(chunk); if (!ferror(file)) { errno = 0; } return NULL; }}
const char *xgetprogname(void) { const char *cmd = NULL;#if BFS_HAS_GETPROGNAME cmd = getprogname();#elif BFS_HAS_GETPROGNAME_GNU cmd = program_invocation_short_name;#endif
if (!cmd) { cmd = BFS_COMMAND; }
return cmd;}
/** Common prologue for xstrto*() wrappers. */static int xstrtox_prologue(const char *str) { // strto*() skips leading spaces, but we want to reject them if (xisspace(str[0])) { errno = EINVAL; return -1; }
errno = 0; return 0;}
/** Common epilogue for xstrto*() wrappers. */static int xstrtox_epilogue(const char *str, char **end, char *endp) { if (errno != 0) { return -1; }
if (end) { *end = endp; }
// If end is NULL, make sure the entire string is valid if (endp == str || (!end && *endp != '\0')) { errno = EINVAL; return -1; }
return 0;}
int xstrtos(const char *str, char **end, int base, short *value) { long n; if (xstrtol(str, end, base, &n) != 0) { return -1; }
if (n < SHRT_MIN || n > SHRT_MAX) { errno = ERANGE; return -1; }
*value = n; return 0;}
int xstrtoi(const char *str, char **end, int base, int *value) { long n; if (xstrtol(str, end, base, &n) != 0) { return -1; }
if (n < INT_MIN || n > INT_MAX) { errno = ERANGE; return -1; }
*value = n; return 0;}
int xstrtol(const char *str, char **end, int base, long *value) { if (xstrtox_prologue(str) != 0) { return -1; }
char *endp; *value = strtol(str, &endp, base); return xstrtox_epilogue(str, end, endp);}
int xstrtoll(const char *str, char **end, int base, long long *value) { if (xstrtox_prologue(str) != 0) { return -1; }
char *endp; *value = strtoll(str, &endp, base); return xstrtox_epilogue(str, end, endp);}
int xstrtof(const char *str, char **end, float *value) { if (xstrtox_prologue(str) != 0) { return -1; }
char *endp; *value = strtof(str, &endp); return xstrtox_epilogue(str, end, endp);}
int xstrtod(const char *str, char **end, double *value) { if (xstrtox_prologue(str) != 0) { return -1; }
char *endp; *value = strtod(str, &endp); return xstrtox_epilogue(str, end, endp);}
int xstrtous(const char *str, char **end, int base, unsigned short *value) { unsigned long n; if (xstrtoul(str, end, base, &n) != 0) { return -1; }
if (n > USHRT_MAX) { errno = ERANGE; return -1; }
*value = n; return 0;}
int xstrtoui(const char *str, char **end, int base, unsigned int *value) { unsigned long n; if (xstrtoul(str, end, base, &n) != 0) { return -1; }
if (n > UINT_MAX) { errno = ERANGE; return -1; }
*value = n; return 0;}
/** Common epilogue for xstrtou*() wrappers. */static int xstrtoux_epilogue(const char *str, char **end, char *endp) { if (xstrtox_epilogue(str, end, endp) != 0) { return -1; }
if (str[0] == '-') { errno = ERANGE; return -1; }
return 0;}
int xstrtoul(const char *str, char **end, int base, unsigned long *value) { if (xstrtox_prologue(str) != 0) { return -1; }
char *endp; *value = strtoul(str, &endp, base); return xstrtoux_epilogue(str, end, endp);}
int xstrtoull(const char *str, char **end, int base, unsigned long long *value) { if (xstrtox_prologue(str) != 0) { return -1; }
char *endp; *value = strtoull(str, &endp, base); return xstrtoux_epilogue(str, end, endp);}
/** Compile and execute a regular expression for xrpmatch(). */static int xrpregex(nl_item item, const char *response) { const char *pattern = nl_langinfo(item); if (!pattern) { return -1; }
struct bfs_regex *regex; int ret = bfs_regcomp(®ex, pattern, BFS_REGEX_POSIX_EXTENDED, 0); if (ret == 0) { ret = bfs_regexec(regex, response, 0); }
bfs_regfree(regex); return ret;}
/** Check if a response is affirmative or negative. */static int xrpmatch(const char *response) { int ret = xrpregex(NOEXPR, response); if (ret > 0) { return 0; } else if (ret < 0) { return -1; }
ret = xrpregex(YESEXPR, response); if (ret > 0) { return 1; } else if (ret < 0) { return -1; }
// Failsafe: always handle y/n char c = response[0]; if (c == 'n' || c == 'N') { return 0; } else if (c == 'y' || c == 'Y') { return 1; } else { return -1; }}
int ynprompt(void) { fflush(stderr);
char *line = xgetdelim(stdin, '\n'); int ret = line ? xrpmatch(line) : -1; free(line); return ret;}
void *xmemdup(const void *src, size_t size) { void *ret = malloc(size); if (ret) { memcpy(ret, src, size); } return ret;}
char *xstpecpy(char *dest, char *end, const char *src) { return xstpencpy(dest, end, src, SIZE_MAX);}
char *xstpencpy(char *dest, char *end, const char *src, size_t n) { size_t space = end - dest; n = space < n ? space : n; n = strnlen(src, n); memcpy(dest, src, n); if (n < space) { dest[n] = '\0'; return dest + n; } else { end[-1] = '\0'; return end; }}
const char *xstrerror(int errnum) { int saved = errno; const char *ret = NULL; static thread_local char buf[256];
// On FreeBSD with MemorySanitizer, duplocale() triggers // https://github.com/llvm/llvm-project/issues/65532#if BFS_HAS_STRERROR_L && !(__FreeBSD__ && __SANITIZE_MEMORY__)# if BFS_HAS_USELOCALE locale_t loc = uselocale((locale_t)0);# else locale_t loc = LC_GLOBAL_LOCALE;# endif
bool free_loc = false; if (loc == LC_GLOBAL_LOCALE) { loc = duplocale(loc); free_loc = true; }
if (loc != (locale_t)0) { ret = strerror_l(errnum, loc); if (free_loc) { freelocale(loc); } }#elif BFS_HAS_STRERROR_R_POSIX if (strerror_r(errnum, buf, sizeof(buf)) == 0) { ret = buf; }#elif BFS_HAS_STRERROR_R_GNU ret = strerror_r(errnum, buf, sizeof(buf));#endif
if (!ret) { // Fallback for strerror_[lr]() or duplocale() failures snprintf(buf, sizeof(buf), "Unknown error %d", errnum); ret = buf; }
errno = saved; return ret;}
const char *errstr(void) { return xstrerror(errno);}
/** Get the single character describing the given file type. */static char type_char(mode_t mode) { switch (mode & S_IFMT) { case S_IFREG: return '-'; case S_IFBLK: return 'b'; case S_IFCHR: return 'c'; case S_IFDIR: return 'd'; case S_IFLNK: return 'l'; case S_IFIFO: return 'p'; case S_IFSOCK: return 's';#ifdef S_IFDOOR case S_IFDOOR: return 'D';#endif#ifdef S_IFPORT case S_IFPORT: return 'P';#endif#ifdef S_IFWHT case S_IFWHT: return 'w';#endif }
return '?';}
void xstrmode(mode_t mode, char str[11]) { strcpy(str, "----------");
str[0] = type_char(mode);
if (mode & 00400) { str[1] = 'r'; } if (mode & 00200) { str[2] = 'w'; } if ((mode & 04100) == 04000) { str[3] = 'S'; } else if (mode & 04000) { str[3] = 's'; } else if (mode & 00100) { str[3] = 'x'; }
if (mode & 00040) { str[4] = 'r'; } if (mode & 00020) { str[5] = 'w'; } if ((mode & 02010) == 02000) { str[6] = 'S'; } else if (mode & 02000) { str[6] = 's'; } else if (mode & 00010) { str[6] = 'x'; }
if (mode & 00004) { str[7] = 'r'; } if (mode & 00002) { str[8] = 'w'; } if ((mode & 01001) == 01000) { str[9] = 'T'; } else if (mode & 01000) { str[9] = 't'; } else if (mode & 00001) { str[9] = 'x'; }}
/** Check if an rlimit value is infinite. */static bool rlim_isinf(rlim_t r) { // Consider RLIM_{INFINITY,SAVED_{CUR,MAX}} all equally infinite if (r == RLIM_INFINITY) { return true; }
#ifdef RLIM_SAVED_CUR if (r == RLIM_SAVED_CUR) { return true; }#endif
#ifdef RLIM_SAVED_MAX if (r == RLIM_SAVED_MAX) { return true; }#endif
return false;}
int rlim_cmp(rlim_t a, rlim_t b) { bool a_inf = rlim_isinf(a); bool b_inf = rlim_isinf(b); if (a_inf || b_inf) { return a_inf - b_inf; }
return (a > b) - (a < b);}
rlim_t rlim_min(rlim_t a, rlim_t b) { if (rlim_cmp(a, b) <= 0) { return a; } else { return b; }}
dev_t xmakedev(int ma, int mi) {#if __QNX__ return makedev(0, ma, mi);#elif defined(makedev) return makedev(ma, mi);#else return (ma << 8) | mi;#endif}
int xmajor(dev_t dev) {#ifdef major return major(dev);#else return dev >> 8;#endif}
int xminor(dev_t dev) {#ifdef minor return minor(dev);#else return dev & 0xFF;#endif}
pid_t xwaitpid(pid_t pid, int *status, int flags) { pid_t ret; do { ret = waitpid(pid, status, flags); } while (ret < 0 && errno == EINTR); return ret;}
int open_cterm(int flags) {#if BFS_HAS_CTERMID char path[L_ctermid]; if (ctermid(path) == NULL || strlen(path) == 0) { errno = ENOTTY; return -1; }#else const char *path = "/dev/tty";#endif
return open(path, flags);}
int xtcgetwinsize(int fd, struct winsize *ws) {#if BFS_HAS_TCGETWINSIZE return tcgetwinsize(fd, ws);#else return ioctl(fd, TIOCGWINSZ, ws);#endif}
int xtcsetwinsize(int fd, const struct winsize *ws) {#if BFS_HAS_TCSETWINSIZE return tcsetwinsize(fd, ws);#else return ioctl(fd, TIOCSWINSZ, ws);#endif}
int dup_cloexec(int fd) {#ifdef F_DUPFD_CLOEXEC return fcntl(fd, F_DUPFD_CLOEXEC, 0);#else int ret = dup(fd); if (ret < 0) { return -1; }
if (fcntl(ret, F_SETFD, FD_CLOEXEC) == -1) { close_quietly(ret); return -1; }
return ret;#endif}
int pipe_cloexec(int pipefd[2]) {#if BFS_HAS_PIPE2 return pipe2(pipefd, O_CLOEXEC);#else if (pipe(pipefd) != 0) { return -1; }
if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1 || fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1) { close_quietly(pipefd[1]); close_quietly(pipefd[0]); return -1; }
return 0;#endif}
size_t xread(int fd, void *buf, size_t nbytes) { size_t count = 0;
while (count < nbytes) { ssize_t ret = read(fd, (char *)buf + count, nbytes - count); if (ret < 0) { if (errno == EINTR) { continue; } else { break; } } else if (ret == 0) { // EOF errno = 0; break; } else { count += ret; } }
return count;}
size_t xwrite(int fd, const void *buf, size_t nbytes) { size_t count = 0;
while (count < nbytes) { ssize_t ret = write(fd, (const char *)buf + count, nbytes - count); if (ret < 0) { if (errno == EINTR) { continue; } else { break; } } else if (ret == 0) { // EOF? errno = 0; break; } else { count += ret; } }
return count;}
void close_quietly(int fd) { int error = errno; xclose(fd); errno = error;}
int xclose(int fd) { int ret = close(fd); if (ret != 0) { bfs_verify(errno != EBADF); } return ret;}
int xfaccessat(int fd, const char *path, int amode) { int ret = faccessat(fd, path, amode, 0);
#ifdef AT_EACCESS // Some platforms, like Hurd, only support AT_EACCESS. Other platforms, // like Android, don't support AT_EACCESS at all. if (ret != 0 && (errno == EINVAL || errno == ENOTSUP)) { ret = faccessat(fd, path, amode, AT_EACCESS); }#endif
return ret;}
char *xconfstr(int name) {#if BFS_HAS_CONFSTR size_t len = confstr(name, NULL, 0); if (len == 0) { return NULL; }
char *str = malloc(len); if (!str) { return NULL; }
if (confstr(name, str, len) != len) { free(str); return NULL; }
return str;#else errno = ENOTSUP; return NULL;#endif}
char *xreadlinkat(int fd, const char *path, size_t size) { ssize_t len; char *name = NULL;
if (size == 0) { size = 64; } else { ++size; // NUL terminator }
while (true) { char *new_name = realloc(name, size); if (!new_name) { goto error; } name = new_name;
len = readlinkat(fd, path, name, size); if (len < 0) { goto error; } else if ((size_t)len >= size) { size *= 2; } else { break; } }
name[len] = '\0'; return name;
error: free(name); return NULL;}
#if BFS_HAS_STRTOFFLAGS# define BFS_STRTOFFLAGS strtofflags#elif BFS_HAS_STRING_TO_FLAGS# define BFS_STRTOFFLAGS string_to_flags#endif
int xstrtofflags(const char **str, unsigned long long *set, unsigned long long *clear) {#ifdef BFS_STRTOFFLAGS char *str_arg = (char *)*str;
#if __OpenBSD__ typedef uint32_t bfs_fflags_t;#else typedef unsigned long bfs_fflags_t;#endif bfs_fflags_t set_arg = 0; bfs_fflags_t clear_arg = 0;
int ret = BFS_STRTOFFLAGS(&str_arg, &set_arg, &clear_arg);
*str = str_arg; *set = set_arg; *clear = clear_arg;
if (ret != 0) { errno = EINVAL; } return ret;#else // !BFS_STRTOFFLAGS errno = ENOTSUP; return -1;#endif}
long xsysconf(int name) {#if __FreeBSD__ && __SANITIZE_MEMORY__ // Work around https://github.com/llvm/llvm-project/issues/88163 __msan_scoped_disable_interceptor_checks();#endif
long ret = sysconf(name);
#if __FreeBSD__ && __SANITIZE_MEMORY__ __msan_scoped_enable_interceptor_checks();#endif
return ret;}
#if BFS_HAS_SCHED_GETAFFINITY/** Get the CPU count in an affinity mask of the given size. */static long bfs_sched_getaffinity(size_t size) { cpu_set_t set, *pset = &set;
if (size > sizeof(set)) { pset = malloc(size); if (!pset) { return -1; } }
long ret = -1; if (sched_getaffinity(0, size, pset) == 0) {# ifdef CPU_COUNT_S ret = CPU_COUNT_S(size, pset);# else bfs_assert(size <= sizeof(set)); ret = CPU_COUNT(pset);# endif }
if (pset != &set) { free(pset); } return ret;}#endif
long nproc(void) { long ret = 0;
#if BFS_HAS_SCHED_GETAFFINITY size_t size = sizeof(cpu_set_t); do { ret = bfs_sched_getaffinity(size);
# ifdef CPU_COUNT_S // On Linux, sched_getaffinity(2) says: // // When working on systems with large kernel CPU affinity masks, one must // dynamically allocate the mask argument (see CPU_ALLOC(3)). Currently, // the only way to do this is by probing for the size of the required mask // using sched_getaffinity() calls with increasing mask sizes (until the // call does not fail with the error EINVAL). size *= 2;# else // No support for dynamically-sized CPU masks break;# endif } while (ret < 0 && errno == EINVAL);#endif
if (ret < 1) { ret = xsysconf(_SC_NPROCESSORS_ONLN); }
if (ret < 1) { ret = 1; }
return ret;}
size_t asciilen(const char *str) { return asciinlen(str, strlen(str));}
size_t asciinlen(const char *str, size_t n) { const unsigned char *ustr = (const unsigned char *)str; size_t i = 0;
// Word-at-a-time isascii()#define CHUNK(n) CHUNK_(uint##n##_t, load8_leu##n)#define CHUNK_(type, load8) \ (n - i >= sizeof(type)) { \ type word = load8(ustr + i); \ type mask = (((type)-1) / 0xFF) << 7; /* 0x808080.. */ \ word &= mask; \ i += trailing_zeros(word) / 8; \ if (word) { \ return i; \ } \ }
#if SIZE_WIDTH >= 64 while CHUNK(64); if CHUNK(32);#else while CHUNK(32);#endif if CHUNK(16); if CHUNK(8);
#undef CHUNK_#undef CHUNK
return i;}
wint_t xmbrtowc(const char *str, size_t *i, size_t len, mbstate_t *mb) { wchar_t wc; size_t mblen = mbrtowc(&wc, str + *i, len - *i, mb); switch (mblen) { case -1: // Invalid byte sequence case -2: // Incomplete byte sequence *i += 1; *mb = (mbstate_t){0}; return WEOF; default: *i += mblen; return wc; }}
size_t xstrwidth(const char *str) { size_t len = strlen(str); size_t ret = 0;
size_t asclen = asciinlen(str, len); size_t i; for (i = 0; i < asclen; ++i) { // Assume all ASCII printables have width 1 if (xisprint(str[i])) { ++ret; } }
mbstate_t mb = {0}; while (i < len) { wint_t wc = xmbrtowc(str, &i, len, &mb); if (wc == (wint_t)WEOF) { // Assume a single-width '?' ++ret; continue; }
int width = xwcwidth(wc); if (width > 0) { ret += width; } }
return ret;}
/** * Character type flags. */enum ctype { IS_PRINT = 1 << 0, IS_SPACE = 1 << 1,};
/** Cached ctypes. */static unsigned char ctype_cache[UCHAR_MAX + 1];
/** Initialize the ctype cache. */static void char_cache_init(void) { for (size_t c = 0; c <= UCHAR_MAX; ++c) { if (xisprint(c)) { ctype_cache[c] |= IS_PRINT; } if (xisspace(c)) { ctype_cache[c] |= IS_SPACE; } }}
/** Check if a character is printable. */static bool wesc_isprint(unsigned char c, enum wesc_flags flags) { if (ctype_cache[c] & IS_PRINT) { return true; }
// Technically a literal newline is safe inside single quotes, but $'\n' // is much nicer than ' // ' if (!(flags & WESC_SHELL) && (ctype_cache[c] & IS_SPACE)) { return true; }
return false;}
/** Check if a wide character is printable. */static bool wesc_iswprint(wchar_t c, enum wesc_flags flags) { if (xiswprint(c)) { return true; }
if (!(flags & WESC_SHELL) && xiswspace(c)) { return true; }
return false;}
/** Get the length of the longest printable prefix of a string. */static size_t printable_len(const char *str, size_t len, enum wesc_flags flags) { static pthread_once_t once = PTHREAD_ONCE_INIT; invoke_once(&once, char_cache_init);
// Fast path: avoid multibyte checks size_t asclen = asciinlen(str, len); size_t i; for (i = 0; i < asclen; ++i) { if (!wesc_isprint(str[i], flags)) { return i; } }
mbstate_t mb = {0}; for (size_t j = i; i < len; i = j) { wint_t wc = xmbrtowc(str, &j, len, &mb); if (wc == (wint_t)WEOF) { break; } if (!wesc_iswprint(wc, flags)) { break; } }
return i;}
/** Convert a special char into a well-known escape sequence like "\n". */static const char *dollar_esc(char c) { // https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html switch (c) { case '\a': return "\\a"; case '\b': return "\\b"; case '\033': return "\\e"; case '\f': return "\\f"; case '\n': return "\\n"; case '\r': return "\\r"; case '\t': return "\\t"; case '\v': return "\\v"; case '\'': return "\\'"; case '\\': return "\\\\"; default: return NULL; }}
/** $'Quote' a string for the shell. */static char *dollar_quote(char *dest, char *end, const char *str, size_t len, enum wesc_flags flags) { dest = xstpecpy(dest, end, "$'");
mbstate_t mb = {0}; for (size_t i = 0; i < len;) { size_t start = i; bool safe = false;
wint_t wc = xmbrtowc(str, &i, len, &mb); if (wc != (wint_t)WEOF) { safe = wesc_iswprint(wc, flags); }
for (size_t j = start; safe && j < i; ++j) { if (str[j] == '\'' || str[j] == '\\') { safe = false; } }
if (safe) { dest = xstpencpy(dest, end, str + start, i - start); } else { for (size_t j = start; j < i; ++j) { unsigned char byte = str[j]; const char *esc = dollar_esc(byte); if (esc) { dest = xstpecpy(dest, end, esc); } else { static const char *hex[] = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"}; dest = xstpecpy(dest, end, "\\x"); dest = xstpecpy(dest, end, hex[byte / 0x10]); dest = xstpecpy(dest, end, hex[byte % 0x10]); } } } }
return xstpecpy(dest, end, "'");}
/** How much of this string is safe as a bare word? */static size_t bare_len(const char *str, size_t len) { // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_02 size_t ret = strcspn(str, "|&;<>()$`\\\"' *?[#~=%!{}"); return ret < len ? ret : len;}
/** How much of this string is safe to double-quote? */static size_t quotable_len(const char *str, size_t len) { // https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_02_03 size_t ret = strcspn(str, "`$\\\"!"); return ret < len ? ret : len;}
/** "Quote" a string for the shell. */static char *double_quote(char *dest, char *end, const char *str, size_t len) { dest = xstpecpy(dest, end, "\""); dest = xstpencpy(dest, end, str, len); return xstpecpy(dest, end, "\"");}
/** 'Quote' a string for the shell. */static char *single_quote(char *dest, char *end, const char *str, size_t len) { bool open = false;
while (len > 0) { size_t chunk = strcspn(str, "'"); chunk = chunk < len ? chunk : len; if (chunk > 0) { if (!open) { dest = xstpecpy(dest, end, "'"); open = true; } dest = xstpencpy(dest, end, str, chunk); str += chunk; len -= chunk; }
while (len > 0 && *str == '\'') { if (open) { dest = xstpecpy(dest, end, "'"); open = false; } dest = xstpecpy(dest, end, "\\'"); ++str; --len; } }
if (open) { dest = xstpecpy(dest, end, "'"); }
return dest;}
char *wordesc(char *dest, char *end, const char *str, enum wesc_flags flags) { return wordnesc(dest, end, str, SIZE_MAX, flags);}
char *wordnesc(char *dest, char *end, const char *str, size_t n, enum wesc_flags flags) { size_t len = strnlen(str, n); char *start = dest;
if (printable_len(str, len, flags) < len) { // String contains unprintable chars, use $'this\x7Fsyntax' dest = dollar_quote(dest, end, str, len, flags); } else if (!(flags & WESC_SHELL) || bare_len(str, len) == len) { // Whole string is safe as a bare word dest = xstpencpy(dest, end, str, len); } else if (quotable_len(str, len) == len) { // Whole string is safe to double-quote dest = double_quote(dest, end, str, len); } else { // Single-quote the whole string dest = single_quote(dest, end, str, len); }
if (dest == start) { dest = xstpecpy(dest, end, "\"\""); }
return dest;}