~strahinja/ste

c4cec2c847a8b13053332b10aca53a93407fe62b — Страхиња Радић 8 months ago 7a3e05c v0.4.0
Make vipe optional by transforming tsv* commands to use stdin/stdout

Signed-off-by: Страхиња Радић <contact@strahinja.org>
11 files changed, 246 insertions(+), 78 deletions(-)

M INSTALL
M TODO
M config.redo
M ste.in
A test/students.tsv
M tsvedit
M tsvedit.1.in
M tsvins
M tsvins.1.in
A tsvselect
A tsvselect.1.in
M INSTALL => INSTALL +2 -2
@@ 4,14 4,14 @@ Prerequisites
* Standard utilities: awk, cat, cp, cut, diff, getopts, grep, head, mv, printf,
  rm, sed, tail, touch, trap, wc
* table[1]
* vipe from moreutils[2]
* Any text editor set via EDITOR environment variable


Optional
--------

* gzip (autodetected, for manpage compression)
* vipe from moreutils[2] + any text editor set via EDITOR environment variable;
  if not present, reads stdin
* rlwrap[3] (autodetected, fallback if absent) for command line editing

[1]: https://strahinja.srht.site/table/

M TODO => TODO +1 -1
@@ 1,7 1,7 @@
TODO
====

< > Transform tsv* to work as stdin/stdout filters
<x> Transform tsv* to work as stdin/stdout filters

< > Command to insert/delete a column?


M config.redo => config.redo +1 -1
@@ 7,6 7,6 @@ MANPREFIX=$PREFIX/share/man
# OpenBSD
#MANPREFIX=$PREFIX/man

PROGS="ste transpose tsvdel tsvedit tsvfind tsvins tsvmove"
PROGS="ste transpose tsvdel tsvedit tsvfind tsvins tsvmove tsvselect"
MANSUFFIX=${MANSUFFIX-$(command -v gzip >/dev/null && printf ".gz")}
MANPAGES=$(for prog in $PROGS; do printf "%s.1%s\n" "$prog" "$MANSUFFIX"; done)

M ste.in => ste.in +44 -5
@@ 25,11 25,16 @@ editprog=tsvedit
findprog=tsvfind
insprog=tsvins 
moveprog=tsvmove
selprog=tsvselect
tableprog=tsvtable

# Variables which normally should not be modified by user
DATE="%DATE%"
VERSION="%VERSION%"
editfilter=''
if command -v vipe >/dev/null 2>&1; then
	editfilter="vipe --suffix tsv"
fi

error()
{


@@ 234,11 239,13 @@ while [ "$running" -eq 1 ]; do

	if [ -z "$nextcmd" ]; then
		if command -v rlwrap >/dev/null; then
			#shellcheck disable=SC2016,SC2140
			cmd=$(rlwrap -H "$historyfile" -s 1000 -D 2 -I -o \
				-S "${file##*/}[${_cols}x${_rows}+${firstrow}"\
"@${currow}/${maxrows}]> " sh -c 'IFS= read -r CMD && printf "%s\n" "$CMD"')
			#shellcheck disable=SC2181
			if [ "$?" -ne 0 ]; then
				cmd=exit
				cmd="exit"
			fi
		else
			printf "%s[%dx%d+%d@%d/%d]> " \


@@ 261,6 268,7 @@ while [ "$running" -eq 1 ]; do
	else
		args=''
	fi
	#shellcheck disable=SC2086
	case "$cmd" in
	/)	set -- $args
		searchcol=''


@@ 341,7 349,14 @@ while [ "$running" -eq 1 ]; do
		fi
		redraw=1
		;;
	edit|e)	${editprog} "$file" "$((currow + 1))"
	edit|e)	if [ "$editfilter" ]; then
			${selprog} "$file" "$((currow + 1))" | ${editfilter} |
				${editprog} "$file" "$((currow + 1))"
		else
			error "warning: vipe not detected; input replaces row"
			${selprog} "$file" "$((currow + 1))"
			cat | ${editprog} "$file" "$((currow + 1))"
		fi
		redraw=1
		;;
	exit|q)	running=0


@@ 366,12 381,36 @@ while [ "$running" -eq 1 ]; do
		;;
	insert|i)
		case "$args" in
		-)	${insprog} "$file" "$((currow))"
		-)	if [ "$editfilter" ]; then
				${selprog} "$file" "$((maxrows + 1))" |
					${editfilter} |
					${insprog} "$file" "$((currow))"
			else
				error "warning: vipe not detected"
				${selprog} "$file" "$((maxrows + 1))"
				cat | ${insprog} "$file" "$((currow))"
			fi
			redraw=1;;
		+)	${insprog} "$file" "$((currow + 1))"
		+)	if [ "$editfilter" ]; then
				${selprog} "$file" "$((maxrows + 1))" |
					${editfilter} |
					${insprog} "$file" "$((currow + 1))"
			else
				error "warning: vipe not detected"
				${selprog} "$file" "$((maxrows + 1))"
				cat | ${insprog} "$file" "$((currow + 1))"
			fi
			currow=$((currow + 1))
			redraw=1;;
		"")	${insprog} "$file"
		"")	if [ "$editfilter" ]; then
				${selprog} "$file" "$((maxrows + 1))" |
					${editfilter} |
					${insprog} "$file"
			else
				error "warning: vipe not detected"
				${selprog} "$file" "$((maxrows + 1))"
				cat | ${insprog} "$file"
			fi
			currow=$((maxrows))
			redraw=1;;
		*)	error "Invalid argument: '%s'" "$args";;

A test/students.tsv => test/students.tsv +3 -0
@@ 0,0 1,3 @@
ID	Name	Age
1	John	25
2	Peter	33

M tsvedit => tsvedit +38 -22
@@ 5,8 5,6 @@
# any later version. Copyright (C) 2023  Страхиња Радић.
# See the file LICENSE for exact copyright and license details.

# Needs vipe from moreutils and $EDITOR https://joeyh.name/code/moreutils/

tab=$(printf "\t")
nl='
'


@@ 37,37 35,55 @@ case $# in
esac

tmpf=${inputfile}~

[ -w "${inputfile}" ] || { error "\`${inputfile}' not writeable"; exit 1; }
command -v vipe >/dev/null 2>&1 || { error "vipe not found"; exit 1; }

cp "${inputfile}" "$tmpf"
# shellcheck disable=SC2064
trap "rm -f \"$tmpf\"" HUP PIPE INT QUIT TERM EXIT
numlines=$(awk 'END {print NR}' "${inputfile}")

if [ -z "${lineno}" ]; then
	lineno=$(cut -d"$tab" -f"${colno}" "${inputfile}" \
		| grep -ni "${searchtext}" \
		| sed 's/^\([0-9]\+\).*/\1/;1q')
	[ -z "$lineno" ] \
		&& { error "not found \`${searchtext}' in column ${colno}"
# shellcheck disable=SC2086
if [ ! $lineno ]; then
	lineno=$(cut -d"$tab" -f"${colno}" -- "${inputfile}" |
		grep -n "${searchtext}" | sed 's/^\([0-9]\+\).*/\1/;1q')
	# shellcheck disable=SC2086
	[ ! $lineno ] &&
		{ error "not found \`${searchtext}' in column ${colno}"
			exit 1; }
fi

savelineno="${lineno}"
# shellcheck disable=SC2030,SC2086
(while [ $lineno ]; do
	case $(printf "%s\n" "${lineno}" | cut -c 1) in
		[0-9])	;;
		*)	error "argument not a number: \`${savelineno}'"
			exit 1
			;;
	esac
	# shellcheck disable=SC2030
	lineno=$(printf "%s\n" "${lineno}" | cut -c 2-)
done) || exit 1

# shellcheck disable=SC2031
if [ "${lineno}" -gt "${numlines}" ]; then
	error "line number ${lineno} greater than the number of lines, \
${numlines}"
	exit 1
fi

# shellcheck disable=SC2031
if [ "${lineno}" -lt 1 ]; then
	error "line number must be positive: \`${lineno}'"
	exit 1
fi

{ head -n$((lineno-1)) "${inputfile}"
  # shellcheck disable=SC1003
  { sed 1q "${inputfile}" | sed -e 's/^/# /' -e 's/'"$tab"'/'"$tab"'# /g'
    head -n"$lineno" "${inputfile}" | tail -n1; } |
  transpose - | sed 's/'"$tab"'/\'"$nl"'/g' |
  vipe --suffix tsv |
touch -- "$tmpf"
# shellcheck disable=SC2064
trap "rm -f \"$tmpf\"" HUP PIPE INT QUIT TERM EXIT

# shellcheck disable=SC2031
{	head -n$((lineno-1)) -- "${inputfile}"
	# shellcheck disable=SC1003
	sed '/^#/d;s/\([^'"$tab"']*\)$/\1'"$tab"'/g' | tr -d '\n' |
	sed 's/'"$tab"'$/\'"$nl"'/g'
  tail +$((lineno+1)) "${inputfile}"; } > "$tmpf"
		sed 's/'"$tab"'$/\'"$nl"'/g'
	tail +$((lineno+1)) -- "${inputfile}"; } > "$tmpf"

if diff "$tmpf" "${inputfile}" >/dev/null 2>&1; then
	rm "$tmpf"

M tsvedit.1.in => tsvedit.1.in +4 -10
@@ 9,26 9,20 @@
.Ar tsvfile.tsv
.Op Ar rowno | Ar colno Ar text
.Sh DESCRIPTION
.
Read lines from stdin and interpret them as columns of a line to replace the
line from a TSV file;
.Bl -tag -width ".Nm Ar rowno" -offset indent
.
.It Nm Ar tsvfile.tsv Ar rowno
select the line (row) to be edited by its row number (1\-based)
select the line (row) to be replaced by its row number (1\-based)
.
.It Nm Ar tsvfile.tsv Ar colno Ar text
select the line (row) to be edited by searching the column with index
select the line (row) to be replaced by searching the column with index
.Ar colno
(1\-based) for
.Ar text .
.
.El
.Sh IMPLEMENTATION NOTES
Needs
.Xr vipe 1
from moreutils
.Lk https://joeyh.name/code/moreutils/
and any
.Ev $EDITOR .
.Sh AUTHORS
.An "Strahinya Radich" Aq contact@strahinja.org ,
2023

M tsvins => tsvins +15 -28
@@ 5,8 5,6 @@
# any later version. Copyright (C) 2023  Страхиња Радић.
# See the file LICENSE for exact copyright and license details.

# Needs vipe from moreutils and $EDITOR https://joeyh.name/code/moreutils/

tab=$(printf "\t")
nl='
'


@@ 25,7 23,7 @@ error()

case $# in
	1)	inputfile=$1
		lineno=0
		lineno=''
		;;
	2)	inputfile=$1
		lineno=$2


@@ 38,37 36,26 @@ esac
[ ! -f "${inputfile}" ] && touch -- "${inputfile}"

tmpf=${inputfile}~
numlines=$(wc -l -- "${inputfile}" | awk '{print $1}')

numlines=$(awk 'END {print NR}' "${inputfile}")
# shellcheck disable=SC2086
[ ! $lineno ] && lineno=$((numlines+1))
command -v vipe >/dev/null 2>&1 || { error "vipe not found"; exit 1; }

cp -- "${inputfile}" "$tmpf"
touch -- "$tmpf"
# shellcheck disable=SC2064
trap "rm -f -- \"$tmpf\"" HUP PIPE INT QUIT TERM EXIT

if [ $# -eq 2 ]; then
	head -n"${lineno}" -- "${inputfile}" > "$tmpf"
fi
if [ "${numlines}" -gt 0 ]; then
{	if [ "${numlines}" -gt 0 ]; then
		if [ "${lineno}" -gt 0 ]; then
			head -n"${lineno}" -- "${inputfile}"
		fi
	fi
	# shellcheck disable=SC1003
	{ sed 1q "${inputfile}" | sed -e 's/^/# /' -e 's/'"$tab"'/'"$tab"'# /g'
	  # shellcheck disable=SC1003
	  sed 1q "${inputfile}" |
	  	sed -e 's/[^'"$tab"']*\('"$tab"'\?\)/\1/g'; } |
		transpose - | sed -e 's/'"$tab"'/\'"$nl"'/g' |
		vipe --suffix tsv |
			sed '/^#/d;s/\([^'"$tab"']*\)$/\1'"$tab"'/g' |
			tr -d '\n' |
			sed 's/'"$tab"'$/\'"$nl"'/g' >> "$tmpf"
else
	# shellcheck disable=SC1003
	vipe --suffix tsv </dev/null |
		sed 's/\([^'"$tab"']*\)$/\1'"$tab"'/g' | tr -d '\n' |
		sed 's/'"$tab"'$/\'"$nl"'/g' >> "$tmpf"
fi
if [ $# -eq 2 ]; then
	tail +$((lineno + 1)) -- "${inputfile}" >> "$tmpf"
fi
	sed '/^#/d;s/\([^'"$tab"']*\)$/\1'"$tab"'/g' | tr -d '\n' |
		sed 's/'"$tab"'$/\'"$nl"'/g'
	if [ "${numlines}" -gt 0 ]; then
		tail +$((lineno + 1)) -- "${inputfile}"
	fi; } > "$tmpf"

if diff -- "$tmpf" "${inputfile}" >/dev/null 2>&1; then
	rm -- "$tmpf"

M tsvins.1.in => tsvins.1.in +2 -9
@@ 9,8 9,8 @@
.Ar tsvfile.tsv
.Op Ar lineno
.Sh DESCRIPTION
.
Insert a line to a TSV file after the line
Read lines from stdin and interpret them as columns of a line to insert to a TSV
file after the line
.Ar lineno
(1-based).
When 0 is passed as


@@ 19,13 19,6 @@ insert a line as first (header) line.
If 
.Ar lineno
is omitted, insert a line at the end of file.
.Sh IMPLEMENTATION NOTES
Needs
.Xr vipe 1
from moreutils
.Lk https://joeyh.name/code/moreutils/
and any
.Ev $EDITOR .
.Sh AUTHORS
.An "Strahinya Radich" Aq contact@strahinja.org ,
2023

A tsvselect => tsvselect +86 -0
@@ 0,0 1,86 @@
#!/bin/sh
# vim: set ft=bash:
# tsvselect - Select a line from a TSV file
# This program is licensed under the terms of GNU GPL v3 or (at your option)
# any later version. Copyright (C) 2023  Страхиња Радић.
# See the file LICENSE for exact copyright and license details.

tab=$(printf "\t")
nl='
'

usage()
{
	cat <<EOT
Usage: ${0##*/} tsvfile.tsv [rowno | colno text]
EOT
}

error()
{
	printf "%s: %s\n" "${0##*/}" "$@" >&2
}

case $# in
	2)	inputfile=$1
		lineno=$2
		;;
	3)	inputfile=$1
		colno=$2
		searchtext=$3
		;;
	*)	usage
		exit 1
		;;
esac

# Exit silently without failing if the file doesn't exist. This enables tsvins
# to work on new files, not existing up to that point
[ ! -f "${inputfile}" ] && exit

[ -w "${inputfile}" ] || { error "\`${inputfile}' not writeable"; exit 1; }
numlines=$(awk 'END {print NR}' "${inputfile}")
command -v vipe >/dev/null 2>&1 || { error "vipe not found"; exit 1; }

# shellcheck disable=SC2086
if [ ! $lineno ]; then
	lineno=$(cut -d"$tab" -f"${colno}" -- "${inputfile}" |
		grep -n "${searchtext}" | sed 's/^\([0-9]\+\).*/\1/;1q')
	# shellcheck disable=SC2086
	[ ! $lineno ] &&
		{ error "not found \`${searchtext}' in column ${colno}"
			exit 1; }
fi

savelineno="${lineno}"
# shellcheck disable=SC2086
(while [ $lineno ]; do
	case $(printf "%s\n" "${lineno}" | cut -c 1) in
		[0-9])	;;
		*)	error "argument not a number: \`${savelineno}'"
			exit 1
			;;
	esac
	# shellcheck disable=SC2030
	lineno=$(printf "%s\n" "${lineno}" | cut -c 2-)
done) || exit 1

# shellcheck disable=SC2031
if [ "${lineno}" -lt 1 ]; then
	error "line number must be positive: \`${lineno}'"
	exit 1
fi

if [ "${numlines}" -gt 0 ]; then
	# shellcheck disable=SC1003
	{	sed 1q "${inputfile}" |
			sed -e 's/^/# /' -e 's/'"$tab"'/'"$tab"'# /g'
		# shellcheck disable=SC2031
		if [ "${lineno}" -le "${numlines}" ]; then
			head -n"${lineno}" -- "${inputfile}" | tail -n1
		else
			sed 1q "${inputfile}" |
				sed -e 's/[^'"$tab"']*\('"$tab"'\?\)/\1/g'
		fi; } |
	transpose - | sed -e 's/'"$tab"'/\'"$nl"'/g'
fi

A tsvselect.1.in => tsvselect.1.in +50 -0
@@ 0,0 1,50 @@
.Dd %DATE%
.Dt TSVSELECT 1
.Os
.Sh NAME
.Nm tsvselect
.Nd Select a line from a TSV file
.Sh SYNOPSIS
.Nm
.Ar tsvfile.tsv
.Op Ar rowno | Ar colno Ar text
.Sh DESCRIPTION
Output a line (row) from a TSV file, with each column being output on a separate
line, and preceded by a line containing a hashmark (
.Li #
) followed by space and 
.Dq column title ;
.Bl -tag -width ".Nm Ar rowno" -offset indent
.
.It Nm Ar tsvfile.tsv Ar rowno
select the line (row) to be output by its row number (1\-based)
.
.It Nm Ar tsvfile.tsv Ar colno Ar text
select the line (row) to be output by searching the column with index
.Ar colno
(1\-based) for
.Ar text .
.
.El
.
For example, for the TSV file
.Pa students.tsv :
.Bd -literal
ID	Name	Age
1	John	25
2	Peter	33
.Ed
.Ql tsvselect students.tsv 3
will output:
.Bd -literal
# ID
2
# Name
Peter
# Age
33
.Ed
.
.Sh AUTHORS
.An "Strahinya Radich" Aq contact@strahinja.org ,
2023