logo

utils-std

Collection of commonly available Unix tools
commit: 7a561b087817f7495ad13a840e57eba79eff67c3
parent e6cbc3a2eb049d5f875b0ae6e9819946fb9c2e2f
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Mon, 27 May 2024 05:33:22 +0200

cmd/head: new

Diffstat:

MMakefile4++++
Acmd/head.185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/head.c214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcoreutils.txt2+-
Mlib/truncation.c2+-
Mlib/truncation.h1+
Mlsb_commands.txt2+-
Mmakeless.sh1+
Mposix_utilities.txt2+-
Mtest-cmd/df4++--
Atest-cmd/head.t68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest-cmd/yes.t6+++---
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 = &copy_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