commit: 7a561b087817f7495ad13a840e57eba79eff67c3
parent e6cbc3a2eb049d5f875b0ae6e9819946fb9c2e2f
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Mon, 27 May 2024 05:33:22 +0200
cmd/head: new
Diffstat:
12 files changed, 382 insertions(+), 9 deletions(-)
diff --git a/Makefile b/Makefile
@@ -133,6 +133,10 @@ cmd/truncate: cmd/truncate.c lib/truncation.c lib/truncation.h Makefile
rm -f ${<:=.gcov} ${@:=.gcda} ${@:=.gcno}
$(CC) -std=c99 $(CFLAGS) -o $@ cmd/truncate.c lib/truncation.c $(LDFLAGS) $(LDSTATIC)
+cmd/head: cmd/head.c lib/truncation.c lib/truncation.h Makefile
+ rm -f ${<:=.gcov} ${@:=.gcda} ${@:=.gcno}
+ $(CC) -std=c99 $(CFLAGS) -o $@ cmd/head.c lib/truncation.c $(LDFLAGS) $(LDSTATIC)
+
cmd/tr: cmd/tr.c lib/tr_str.c lib/tr_str.h Makefile
rm -f ${<:=.gcov} ${@:=.gcda} ${@:=.gcno}
$(CC) -std=c99 $(CFLAGS) -o $@ cmd/tr.c lib/tr_str.c $(LDFLAGS) $(LDSTATIC)
diff --git a/cmd/head.1 b/cmd/head.1
@@ -0,0 +1,85 @@
+.\" utils-std: Collection of commonly available Unix tools
+.\" Copyright 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+.\" SPDX-License-Identifier: MPL-2.0
+.Dd 2024-05-27
+.Dt HEAD 1
+.Os
+.Sh NAME
+.Nm head
+.Nd print first part of files
+.Sh SYNOPSIS
+.Nm
+.Op Fl qv
+.Op Fl c Ar size | Fl n Ar num
+.Op Ar file...
+.Sh DESCRIPTION
+.Nm
+reads each
+.Ar file
+in sequence and writes it's first part on standard output.
+If no
+.Ar file
+is given,
+.Nm
+reads from the standard input.
+.Sh OPTIONS
+.Bl -tag -width _c_size
+.It Fl c Ar size
+Read the first
+.Ar size
+bytes
+in each
+.Ar file .
+.Pp
+.Ar size
+can be multiplied by one of KMGTP (Kilo, Mega, Giga, Tera, ...)
+using power of 1024 by default, which can be either explicit via adding iB
+(like MiB), or turned into powers of 1000 by adding B (like MB).
+.It Fl n Ar num
+Read the first
+.Ar num
+lines in each
+.Ar file .
+.It Fl q
+Don't print header when multiple
+.Ar file
+are given.
+.It Fl v
+Always print header, regardless of the number of given
+.Ar file .
+.El
+.Pp
+If no option is specified,
+.Nm
+defaults to reading the first 10 lines.
+.Sh EXIT STATUS
+.Ex -std
+.Sh STDOUT
+Contains the specified part of each
+.Ar file ,
+with the following header when multiple
+.Ar file
+are given, or when
+.Fl v
+is set:
+.Bd -literal
+"==> %s <==\\n", <file>
+.Ed
+.Sh STANDARDS
+.Nm
+should be compliant with the
+.St -p1003.1-2008
+specification.
+.br
+The
+.Fl c
+option is from
+IEEE P1003.1-202x/D4 (“POSIX.1”).
+.br
+The
+.Fl q
+and
+.Fl v
+options are extensions.
+.Sh AUTHORS
+.An Haelwenn (lanodan) Monnier Aq Mt contact+utils@hacktivis.me
diff --git a/cmd/head.c b/cmd/head.c
@@ -0,0 +1,214 @@
+// utils-std: Collection of commonly available Unix tools
+// SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+// SPDX-License-Identifier: MPL-2.0
+
+#define _POSIX_C_SOURCE 200809L
+
+#include "../lib/truncation.h" // apply_size_suffix
+
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h> // fprintf, fopen
+#include <stdlib.h> // strtoul
+#include <string.h> // strerror
+#include <unistd.h> // read
+
+static char *header_fmt = "==> %s <==\n";
+
+char *argv0 = "head";
+
+size_t lines = 10;
+
+char *buf = NULL;
+size_t buflen = 0;
+
+static int
+copy_bytes(FILE *in, char *filename)
+{
+ assert(errno == 0);
+ size_t eread = fread(buf, sizeof(char), buflen, in);
+ if(errno != 0)
+ {
+ fprintf(stderr, "head: Failed reading file '%s': %s\n", filename, strerror(errno));
+ return 1;
+ }
+
+ if(write(STDOUT_FILENO, buf, eread) < 0)
+ {
+ fprintf(
+ stderr, "head: Failed writing line from '%s' to stdout: %s\n", filename, strerror(errno));
+ return 1;
+ }
+
+ return 0;
+}
+
+static int
+copy_lines(FILE *in, char *filename)
+{
+ for(size_t i = 0; i < lines; i++)
+ {
+ assert(errno == 0);
+ ssize_t nread = getline(&buf, &buflen, in);
+ if(nread < 0)
+ {
+ if(errno == 0) break;
+
+ fprintf(stderr, "head: Failed reading line from '%s': %s\n", filename, strerror(errno));
+ return 1;
+ }
+
+ if(write(STDOUT_FILENO, buf, nread) < 0)
+ {
+ fprintf(
+ stderr, "head: Failed writing line from '%s' to stdout: %s\n", filename, strerror(errno));
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static void
+usage()
+{
+ fprintf(stderr, "Usage: head [-qv] [-c size | -n num] [file...]\n");
+}
+
+int
+main(int argc, char *argv[])
+{
+ int (*copy_action)(FILE *in, char *filename) = copy_lines;
+
+ int print_header = 0;
+ int c = -1;
+ while((c = getopt(argc, argv, ":qvc:n:")) != -1)
+ {
+ switch(c)
+ {
+ case 'q':
+ print_header = -1;
+ break;
+ case 'v':
+ print_header = 1;
+ break;
+ case 'c':
+ {
+ assert(errno == 0);
+ char *endptr = NULL;
+ long size = strtol(optarg, &endptr, 0);
+ if(errno != 0)
+ {
+ fprintf(
+ stderr, "head: Error while parsing number for `-n %s`: %s\n", optarg, strerror(errno));
+ return 1;
+ }
+
+ if(endptr != NULL && *endptr != 0)
+ if(apply_size_suffix(&size, endptr) != 0) return 1;
+
+ buflen = size;
+ copy_action = ©_bytes;
+ break;
+ }
+ case 'n':
+ {
+ assert(errno == 0);
+ char *endptr = NULL;
+ lines = strtoul(optarg, &endptr, 0);
+ if(errno != 0)
+ {
+ fprintf(
+ stderr, "head: Error while parsing number for `-n %s`: %s\n", optarg, strerror(errno));
+ return 1;
+ }
+ break;
+ }
+ case ':':
+ fprintf(stderr, "head: Error: Missing operand for option: '-%c'\n", optopt);
+ usage();
+ return 1;
+ case '?':
+ fprintf(stderr, "head: Error: Unrecognised option: '-%c'\n", optopt);
+ usage();
+ return 1;
+ default:
+ abort();
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if(buflen != 0)
+ {
+ buf = malloc(buflen);
+
+ if(buf == NULL)
+ {
+ fprintf(stderr, "head: Failed to allocate buffer: %s\n", strerror(errno));
+ return 1;
+ }
+ }
+
+ if(argc <= 0)
+ {
+ char *filename = "<stdin>";
+
+ if(print_header == 1)
+ {
+ printf(header_fmt, filename);
+ fflush(stdout);
+ }
+
+ int err = copy_action(stdin, filename);
+
+ if(buflen != 0) free(buf);
+
+ return err;
+ }
+
+ if(print_header == 0) print_header = argc > 1;
+
+ int err = 0;
+
+ for(int i = 0; i < argc; i++)
+ {
+ char *filename = argv[i];
+
+ if(print_header > 0)
+ {
+ printf(header_fmt, filename);
+ header_fmt = "\n==> %s <==\n";
+ fflush(stdout);
+ }
+
+ if(filename[0] == '-' && filename[1] == 0)
+ {
+ if(copy_action(stdin, "<stdin>") != 0) break;
+
+ continue;
+ }
+
+ FILE *in = fopen(filename, "r");
+ if(in == NULL)
+ {
+ fprintf(stderr, "head: Failed opening file '%s': %s\n", filename, strerror(errno));
+ err = 1;
+ break;
+ }
+
+ if(copy_action(in, filename) != 0) return 1;
+
+ if(fclose(in) != 0)
+ {
+ fprintf(stderr, "head: Failed closing file '%s': %s\n", filename, strerror(errno));
+ err = 1;
+ break;
+ }
+ }
+
+ if(buflen != 0) free(buf);
+
+ return err;
+}
diff --git a/coreutils.txt b/coreutils.txt
@@ -33,7 +33,7 @@ factor: ?
false: Done
fmt: ?
fold: ?
-head: No, use sed
+head: Done
hostid: No, get a better id(ea)
id: Done
install: Done
diff --git a/lib/truncation.c b/lib/truncation.c
@@ -67,7 +67,7 @@ apply_truncation(int fd, struct truncation tr, char *arg)
return 0;
}
-static int
+int
apply_size_suffix(long *size, char *endptr)
{
char units[] = "KMGTPEZ";
diff --git a/lib/truncation.h b/lib/truncation.h
@@ -22,4 +22,5 @@ struct truncation
};
int parse_size(char *arg, struct truncation *buf);
+int apply_size_suffix(long *size, char *endptr);
int apply_truncation(int fd, struct truncation tr, char *arg);
diff --git a/lsb_commands.txt b/lsb_commands.txt
@@ -51,7 +51,7 @@ groupmod: out of scope
groups: Probably not
gunzip: out of scope
gzip: out of scope
-head: No, use sed
+head: Done
hostname: Maybe
iconv: out of scope
id: Done
diff --git a/makeless.sh b/makeless.sh
@@ -24,6 +24,7 @@ $CC -std=c99 $CFLAGS -o cmd/dirname cmd/dirname.c $LDFLAGS $LDSTATIC
$CC -std=c99 $CFLAGS -o cmd/echo cmd/echo.c $LDFLAGS $LDSTATIC
$CC -std=c99 $CFLAGS -o cmd/env cmd/env.c $LDFLAGS $LDSTATIC
$CC -std=c99 $CFLAGS -o cmd/false cmd/false.c $LDFLAGS $LDSTATIC
+$CC -std=c99 $CFLAGS -o cmd/head cmd/head.c lib/truncation.c $LDFLAGS $LDSTATIC
$CC -std=c99 $CFLAGS -o cmd/id cmd/id.c $LDFLAGS $LDSTATIC
$CC -std=c99 $CFLAGS -o cmd/install cmd/install.c lib/mode.c lib/user_group_parse.c lib/fs.c lib/mkdir.c $LDFLAGS $LDSTATIC
$CC -std=c99 $CFLAGS -o cmd/link cmd/link.c $LDFLAGS $LDSTATIC
diff --git a/posix_utilities.txt b/posix_utilities.txt
@@ -58,7 +58,7 @@ getconf: NetBSD getconf adapted for musl by Alpine is okay
getopts: no, sh built-in
grep: no?
hash: no, sh built-in
-head
+head: done
iconv: no, external
id: done
ipcrm
diff --git a/test-cmd/df b/test-cmd/df
@@ -7,8 +7,8 @@ posix_body() {
# stderr ignored because not all filesystems allow statvfs from a normal user.
# For example tracefs aka "/sys/kernel/debug/tracing" on Linux.
- atf_check -e ignore -o 'match:Filesystem 512-blocks Used Available Capacity Mounted on' sh -c '../cmd/df -P | head -1'
- atf_check -e ignore -o 'match:Filesystem 1024-blocks Used Available Capacity Mounted on' sh -c '../cmd/df -Pk | head -1'
+ atf_check -e ignore -o 'match:Filesystem 512-blocks Used Available Capacity Mounted on' sh -c '../cmd/df -P | head -n 1'
+ atf_check -e ignore -o 'match:Filesystem 1024-blocks Used Available Capacity Mounted on' sh -c '../cmd/df -Pk | head -n 1'
atf_check -e ignore -o 'match:^[^ ]* [0-9]* [0-9]* [0-9]* [0-9]*% [^ ]$' sh -c '../cmd/df -P | sed -n 2,\$p'
atf_check -e ignore -o 'match:^[^ ]* [0-9]* [0-9]* [0-9]* [0-9]*% [^ ]$' sh -c '../cmd/df -Pk | sed -n 2,\$p'
diff --git a/test-cmd/head.t b/test-cmd/head.t
@@ -0,0 +1,68 @@
+#!/usr/bin/env cram
+# SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+# SPDX-License-Identifier: MPL-2.0
+
+ $ export PATH="$TESTDIR/../cmd:$PATH"
+
+ $ test "$(command -v head)" = "$TESTDIR/../cmd/head"
+
+ $ set -o pipefail
+
+ $ seq 1 20 | head
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+
+ $ seq 1 20 | head -n 5
+ 1
+ 2
+ 3
+ 4
+ 5
+
+ $ seq 1 5 | head
+ 1
+ 2
+ 3
+ 4
+ 5
+
+ $ seq 1 5 | head -n 40
+ 1
+ 2
+ 3
+ 4
+ 5
+
+
+ $ </dev/zero head -c 40 | wc -c
+ 40
+ $ </dev/zero head -c 40K | wc -c
+ 40960
+ $ </dev/zero head -c 40k | wc -c
+ head: Unrecognised unit 'k'
+ 0
+ [1]
+
+ $ seq 1 2 | head - /dev/null
+ ==> - <==
+ 1
+ 2
+
+ ==> /dev/null <==
+
+ $ seq 1 2 | head -v -
+ ==> - <==
+ 1
+ 2
+
+ $ seq 1 2 | head -q - /dev/null
+ 1
+ 2
diff --git a/test-cmd/yes.t b/test-cmd/yes.t
@@ -6,16 +6,16 @@
$ test "$(command -v yes)" = "$TESTDIR/../cmd/yes"
- $ yes | head -3
+ $ yes | head -n 3
y
y
y
- $ yes foo | head -4
+ $ yes foo | head -n 4
foo
foo
foo
foo
- $ yes $'foo\nbar\n' | head -5
+ $ yes $'foo\nbar\n' | head -n 5
foo
bar