diff options
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | LICENSE | 14 | ||||
-rw-r--r-- | Makefile | 15 | ||||
-rw-r--r-- | center.1 | 79 | ||||
-rw-r--r-- | center.c | 163 |
5 files changed, 275 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8166a02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.cache/ +.clang-format +center +compile_commands.json @@ -0,0 +1,14 @@ +BSD Zero Clause License + +Copyright (c) 2022 Thomas Voss + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ad0a89c --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.POSIX: + +CFLAGS = -Ofast -march=native -mtune=native -pipe +PREFIX = /usr + +all: center +center: center.c + +clean: + rm -f center + +install: + mkdir -p ${PREFIX}/bin ${PREFIX}/share/man/man1 + cp center ${PREFIX}/bin + cp center.1 ${PREFIX}/share/man/man1 diff --git a/center.1 b/center.1 new file mode 100644 index 0000000..e3704bd --- /dev/null +++ b/center.1 @@ -0,0 +1,79 @@ +.Dd $Mdocdate: February 7 2022 $ +.Dt CENTER 1 +.Os +.Sh NAME +.Nm center +.Nd center align text +.Sh SYNOPSIS +.Nm +.Op Fl e +.Op Fl w Ar width +.Op Ar +.Sh DESCRIPTION +The +.Nm +utility reads files sequentially, writing each line center aligned to the standard output. +The +.Ar file +operands are processed in command-line order. +If +.Ar file +is a single dash +.Pq Sq - +or absent, +.Nm +reads from the standard input. +When run without arguments, the width on which the output should be centered is automatically +determined. +When the output device is a TTY, this is done via the +.Fn isatty +function. +When the output device isn't a terminal then the user is required to pass a width via the +.Fl w +flag. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl e +Do not take ANSI color escape sequences into account when centering input. +This will cause input containing ANSI color escape sequences to fail to be visually centered. +.It Fl w Ar width +Center align the input as if the output device has a width of +.Ar width . +This option is required when the output device is not a terminal. +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +Center align the contents of +.Ar file1 +and +.Ar file2 +to the standard output: +.Pp +.Dl $ center file1 file2 +.Pp +Center align a calendar and then follow it with the contents of the file +.Ar file1 , +writing the output to the file +.Ar file2 . +Align the calendar as if the output device has a width of 80 columns. +.Pp +.Dl $ cal | center | cat - file1 > file2 +.Pp +Center align the contents of +.Ar file1 +and write the output to +.Ar file2 +as if the output device had the same width as the current TTY. +This requires non\-standard extensions, and you should refer to the +.Xr tput 1 +manual page. +.Pp +.Dl $ center -w `tput cols` file1 > file2 +.Sh SEE ALSO +.Xr cal 1 , +.Xr cat 1 , +.Xr tput 1 , +.Xr isatty 3 +.Sh AUTHORS +.An Thomas Voss Aq Mt thomasvoss@live.com diff --git a/center.c b/center.c new file mode 100644 index 0000000..ce0d50e --- /dev/null +++ b/center.c @@ -0,0 +1,163 @@ +#define POSIXLY_CORRECT +#define _POSIX_C_SOURCE 200809L + +#include <sys/ioctl.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <limits.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define ESC 033 + +static void center(FILE *); +static int cols(void); +static int utf8len(const char *); +static int noesclen(const char *); +static int matchesc(const char *); + +extern int optind; +extern char *optarg; + +int rval; +long width; +int (*lenfunc)(const char *) = noesclen; + +int +main(int argc, char **argv) +{ + int opt; + char *endptr; + + while ((opt = getopt(argc, argv, ":elsw:")) != -1) { + switch (opt) { + case 'e': + lenfunc = utf8len; + break; + case 'w': + width = strtol(optarg, &endptr, 0); + if (*optarg == '\0' || *endptr != '\0') + errx(EXIT_FAILURE, "Invalid integer '%s'", optarg); + if (width <= 0) + errx(EXIT_FAILURE, "Width must be >0"); + if (errno == ERANGE || width > INT_MAX) + warnx("Potential overflow of given width"); + break; + default: + fprintf(stderr, "Usage: %s [-e] [-w width]\n", argv[0]); + exit(EXIT_FAILURE); + } + } + + if (!width && (width = cols()) == -1) + errx(EXIT_FAILURE, "Unable to determine output width"); + + argc -= optind; + argv += optind; + + if (argc == 0) + center(stdin); + else do { + if (strcmp(*argv, "-") == 0) + center(stdin); + else { + FILE *fp; + if ((fp = fopen(*argv, "r")) == NULL) + errx(EXIT_FAILURE, "fopen"); + center(fp); + fclose(fp); + } + } while (*++argv); + + return rval; +} + +/* Write the contents of the file pointed to by `fp' center aligned to the standard output */ +void +center(FILE *fp) +{ + char *line = NULL; + size_t bs = 0; + + while (getline(&line, &bs, fp) != -1) { + int len, tabs = 0; + char *match = line; + + while (strchr(match, '\t')) { + match++; + tabs++; + } + + len = lenfunc(line) + tabs * 8 - tabs + 1; + printf("%*s", ((int) width - len) / 2 + len, line); + } + if (ferror(fp)) { + warn("getline"); + rval = EXIT_FAILURE; + } +} + +/* Return the column width of the output device if it is a TTY. If it is not a TTY then return -1 */ +int +cols(void) +{ + struct winsize w; + + if (!isatty(STDOUT_FILENO)) + return -1; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == -1) + err(EXIT_FAILURE, "ioctl"); + + return w.ws_col; +} + +/* Return the length of the UTF-8 encoded string `s' in characters, not bytes */ +int +utf8len(const char *s) +{ + int l = 0; + + while (*s) + l += (*s++ & 0xC0) != 0x80; + + return l; +} + +/* Return the length of the UTF-8 encoded string `s' in characters, not bytes. Additionally this + * function ignores ANSI color escape sequences when computing the length. + */ +int +noesclen(const char *s) +{ + int off = 0; + const char *d = s; + + while ((d = strchr(d, ESC)) != NULL) + off += matchesc(++d); + + return utf8len(s) + off; +} + +/* Return the length of the ANSI color escape sequence at the beginning of the string `s'. If no + * escape sequence is matched then 0 is returned. The local variable `c' is initialized to 3 in order + * to account for the leading ESC, the following `[', and the trailing `m'. + */ +int +matchesc(const char *s) +{ + int c = 3; + + if (*s++ != '[') + return 0; + while (isdigit(*s) || *s == ';') { + c++; + s++; + } + + return *s == 'm' ? c : 0; +} |