logo

utils-std

Collection of commonly available Unix tools git clone https://anongit.hacktivis.me/git/utils-std.git/

wc.c (6918B)


  1. // utils-std: Collection of commonly available Unix tools
  2. // SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
  3. // SPDX-License-Identifier: MPL-2.0
  4. #define _POSIX_C_SOURCE 200809L
  5. #include "../config.h" // HAS_*
  6. #include "../lib/bitmasks.h"
  7. #include "../libutils/getopt_nolong.h"
  8. #include <ctype.h> // isspace
  9. #include <errno.h>
  10. #include <fcntl.h> // posix_fadvise
  11. #include <locale.h> // setlocale
  12. #include <stdbool.h>
  13. #include <stdint.h> // uint8_t
  14. #include <stdio.h> // fprintf, fopen
  15. #include <stdlib.h> // abort
  16. #include <string.h> // strchr, strerror
  17. #include <sys/stat.h>
  18. #include <unistd.h> // getopt
  19. #include <wchar.h>
  20. #include <wctype.h> // iswspace
  21. #ifdef HAS_GETOPT_LONG
  22. #include <getopt.h>
  23. #endif
  24. #define WC_BUFSIZ 16320
  25. static char buf[WC_BUFSIZ] = "";
  26. const char *argv0 = "wc";
  27. static enum {
  28. WC_OPT_C = 1 << 0,
  29. WC_OPT_L = 1 << 1,
  30. WC_OPT_W = 1 << 2,
  31. WC_OPT_ALL = WC_OPT_C | WC_OPT_L | WC_OPT_W,
  32. } wc_opts = 0;
  33. off_t total_bytes = 0, total_lines = 0, total_words = 0;
  34. static void
  35. print_counts(off_t lines, off_t words, off_t bytes, const char *filename)
  36. {
  37. const char *fmt = "%ld";
  38. if(FIELD_MATCH(wc_opts, WC_OPT_L))
  39. {
  40. printf(fmt, lines);
  41. fmt = " %ld";
  42. }
  43. if(FIELD_MATCH(wc_opts, WC_OPT_W))
  44. {
  45. printf(fmt, words);
  46. fmt = " %ld";
  47. }
  48. if(FIELD_MATCH(wc_opts, WC_OPT_C))
  49. {
  50. printf(fmt, bytes);
  51. }
  52. if(filename != NULL) printf(" %s", filename);
  53. printf("\n");
  54. }
  55. static int
  56. wc_file_bytes(int fd, char *filename)
  57. {
  58. off_t bytes = 0, lines = 0, words = 0, wordlen = 0;
  59. ssize_t nread = -1;
  60. if(FIELD_MATCH(wc_opts, WC_OPT_W))
  61. {
  62. while((nread = read(fd, buf, WC_BUFSIZ)) > 0)
  63. {
  64. bytes += nread;
  65. for(ssize_t i = 0; i < nread; i++)
  66. {
  67. int c = buf[i];
  68. if(c == '\n') lines++;
  69. if(isspace(c))
  70. {
  71. if(wordlen > 0)
  72. {
  73. words++;
  74. wordlen = 0;
  75. }
  76. }
  77. else
  78. {
  79. wordlen++;
  80. }
  81. }
  82. }
  83. }
  84. else if(FIELD_MATCH(wc_opts, WC_OPT_L))
  85. {
  86. while((nread = read(fd, buf, WC_BUFSIZ)) > 0)
  87. {
  88. bytes += nread;
  89. for(ssize_t i = 0; i < nread; i++)
  90. if(buf[i] == '\n') lines++;
  91. }
  92. }
  93. else
  94. {
  95. struct stat status;
  96. if(fstat(fd, &status) < 0)
  97. {
  98. fprintf(stderr,
  99. "%s: error: Failed getting status for file '%s': %s\n",
  100. argv0,
  101. filename,
  102. strerror(errno));
  103. return 1;
  104. }
  105. if(S_ISREG(status.st_mode) && status.st_size != 0)
  106. {
  107. bytes += status.st_size;
  108. }
  109. else
  110. {
  111. while((nread = read(fd, buf, WC_BUFSIZ)) > 0)
  112. bytes += nread;
  113. }
  114. }
  115. if(nread < 0 && errno != 0)
  116. {
  117. fprintf(stderr,
  118. "%s: error: Failed reading from file '%s': %s\n",
  119. argv0,
  120. filename != NULL ? filename : "<stdin>",
  121. strerror(errno));
  122. return -1;
  123. }
  124. if(wordlen > 0) words++;
  125. total_bytes += bytes, total_lines += lines, total_words += words;
  126. print_counts(lines, words, bytes, filename);
  127. return 0;
  128. }
  129. static int
  130. wc_file_chars(int fd, char *filename)
  131. {
  132. off_t chars = 0, lines = 0, words = 0, wordlen = 0;
  133. FILE *file = fdopen(fd, "r");
  134. if(file == NULL)
  135. {
  136. fprintf(stderr,
  137. "%s: error: Failed getting file stream for file '%s': %s\n",
  138. argv0,
  139. filename,
  140. strerror(errno));
  141. return -1;
  142. }
  143. if(setvbuf(file, buf, _IOFBF, WC_BUFSIZ) != 0)
  144. {
  145. fprintf(stderr,
  146. "%s: warning: Failed setting a new buffer for <stdin>: %s\n",
  147. argv0,
  148. strerror(errno));
  149. errno = 0;
  150. }
  151. while(true)
  152. {
  153. wint_t c = getwc(file);
  154. if(c == WEOF)
  155. {
  156. if(errno != 0)
  157. {
  158. fprintf(stderr,
  159. "%s: error: Failed reading from file '%s': %s\n",
  160. argv0,
  161. filename != NULL ? filename : "<stdin>",
  162. strerror(errno));
  163. return -1;
  164. }
  165. break;
  166. }
  167. chars++;
  168. if(c == '\n') lines++;
  169. if(iswspace(c))
  170. {
  171. if(wordlen > 0)
  172. {
  173. words++;
  174. wordlen = 0;
  175. }
  176. }
  177. else
  178. {
  179. wordlen++;
  180. }
  181. }
  182. if(wordlen > 0) words++;
  183. print_counts(lines, words, chars, filename);
  184. return 0;
  185. }
  186. static void
  187. usage(void)
  188. {
  189. fprintf(stderr, "Usage: wc [-c|-m] [-lw] [file...]\n");
  190. }
  191. int
  192. main(int argc, char *argv[])
  193. {
  194. char *lc_all = setlocale(LC_ALL, "");
  195. if(lc_all == NULL)
  196. {
  197. fprintf(stderr,
  198. "%s: warning: Failed loading locales. setlocale(LC_ALL, \"\"): %s\n",
  199. argv0,
  200. strerror(errno));
  201. }
  202. errno = 0;
  203. int (*wc_file)(int, char *) = &wc_file_bytes;
  204. #ifdef HAS_GETOPT_LONG
  205. // Strictly for GNUisms compatibility so no long-only options
  206. // clang-format off
  207. static struct option opts[] = {
  208. {"bytes", no_argument, 0, 'c'},
  209. {"lines", no_argument, 0, 'l'},
  210. {"chars", no_argument, 0, 'm'},
  211. {"words", no_argument, 0, 'w'},
  212. {0, 0, 0, 0},
  213. };
  214. // clang-format on
  215. // Need + as first character to get POSIX-style option parsing
  216. for(int c = -1; (c = getopt_long(argc, argv, "+:clmw", opts, NULL)) != -1;)
  217. #else
  218. for(int c = -1; (c = getopt_nolong(argc, argv, ":clmw")) != -1;)
  219. #endif
  220. {
  221. switch(c)
  222. {
  223. case 'c':
  224. wc_opts |= WC_OPT_C;
  225. wc_file = wc_file_bytes;
  226. break;
  227. case 'l':
  228. wc_opts |= WC_OPT_L;
  229. break;
  230. case 'm':
  231. wc_opts |= WC_OPT_C;
  232. wc_file = wc_file_chars;
  233. break;
  234. case 'w':
  235. wc_opts |= WC_OPT_W;
  236. break;
  237. case ':':
  238. fprintf(stderr, "%s: error: Missing operand for option: '-%c'\n", argv0, optopt);
  239. usage();
  240. return 1;
  241. case '?':
  242. GETOPT_UNKNOWN_OPT
  243. usage();
  244. return 1;
  245. default:
  246. abort();
  247. }
  248. }
  249. if(wc_opts == 0) wc_opts = WC_OPT_ALL;
  250. argc -= optind;
  251. argv += optind;
  252. if((errno = posix_fadvise(STDIN_FILENO, 0, 0, POSIX_FADV_SEQUENTIAL)) != 0)
  253. {
  254. if(errno != ESPIPE)
  255. {
  256. fprintf(stderr,
  257. "%s: warning: Failure from posix_fadvise sequential for <stdin>: %s\n",
  258. argv0,
  259. strerror(errno));
  260. }
  261. errno = 0;
  262. }
  263. if(argc < 1)
  264. {
  265. if(wc_file(STDIN_FILENO, NULL) < 0) return 1;
  266. }
  267. for(int i = 0; i < argc; i++)
  268. {
  269. char *path = argv[i];
  270. if(path[0] == '-' && path[1] == 0)
  271. {
  272. if(wc_file(STDIN_FILENO, NULL) < 0) return 1;
  273. continue;
  274. }
  275. // https://www.austingroupbugs.net/view.php?id=251
  276. if(strchr(path, '\n') != NULL)
  277. fprintf(
  278. stderr,
  279. "%s: warning: Filename '%s' contains a newline while wc(1) uses newlines as separators\n",
  280. argv0,
  281. path);
  282. int arg_fd = open(path, O_RDONLY | O_NOCTTY);
  283. if(arg_fd < 0)
  284. {
  285. fprintf(stderr, "%s: error: Failed opening file '%s': %s\n", argv0, path, strerror(errno));
  286. return 1;
  287. }
  288. if((errno = posix_fadvise(arg_fd, 0, 0, POSIX_FADV_SEQUENTIAL)) != 0)
  289. {
  290. fprintf(stderr,
  291. "%s: warning: Failure from posix_fadvise sequential for file '%s': %s\n",
  292. argv0,
  293. path,
  294. strerror(errno));
  295. errno = 0;
  296. }
  297. if(wc_file(arg_fd, path) < 0) return 1;
  298. if(close(arg_fd) < 0)
  299. {
  300. fprintf(stderr, "%s: error: Failed closing file '%s': %s\n", argv0, path, strerror(errno));
  301. return 1;
  302. }
  303. }
  304. if(argc > 1) print_counts(total_lines, total_words, total_bytes, "total");
  305. return 0;
  306. }