aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--LICENSE14
-rw-r--r--Makefile15
-rw-r--r--center.179
-rw-r--r--center.c163
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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6117b2a
--- /dev/null
+++ b/LICENSE
@@ -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;
+}