Compare commits

...

22 Commits

Author SHA1 Message Date
7566f9af82 liberty: comment on pthread_cancel
All checks were successful
Alpine 3.21 Success
OpenBSD 7.6 Success
2025-09-21 18:59:16 +02:00
7425355d01 liberty-xui: fix a new Fontconfig warning
All checks were successful
Alpine 3.21 Success
OpenBSD 7.6 Success
2025-08-02 18:22:00 +02:00
d8f785eae5 liberty-xdg: don't crash on missing X11 atoms
All checks were successful
Alpine 3.21 Success
OpenBSD 7.6 Success
They can be missing in bare configurations, such as Sway + XWayland.
2025-06-04 21:54:04 +02:00
31ae400852 LibertyXDR: update VIM syntax highlight file
All checks were successful
Alpine 3.21 Success
OpenBSD 7.6 Success
2025-05-07 19:47:43 +02:00
b69d3f8692 LibertyXDR: add support for default in unions 2025-05-07 19:42:46 +02:00
9a26284a64 wdye: clean up protected calls
Have a common way of catching Lua errors for resource cleanup purposes.
2025-01-15 02:21:23 +01:00
0f20cce9c8 wdye: pass script arguments
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2025-01-10 10:58:27 +01:00
017cb1d570 MPD client: tolerate usage while disconnected
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
If the client is password-protected, this will not automagically
make queued up commands work, but it's better than hitting
the poller assertion.
2025-01-08 08:07:46 +01:00
1642d387f3 wdye: rename the self-test
add_subdirectory imports it to parent projects, so be more indicative.
2025-01-08 06:24:05 +01:00
af889b733e wdye: ensure we find our own config.h
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2025-01-08 06:14:47 +01:00
51231d84ba wdye: clean up, add process.pid
All checks were successful
OpenBSD 7.5 Success
Alpine 3.20 Success
2025-01-07 03:16:37 +01:00
6c47e384f5 wdye: optionally produce asciicast v2 logs
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
I've been fairly disappointed with asciinema,
but it's slightly better than nothing.
2025-01-06 17:03:54 +01:00
914e743dc4 wdye: don't add the script path on error
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
Lua already provides this for us, including the line number.
2025-01-06 14:40:58 +01:00
37a8f16235 wdye: enable waiting for processes 2025-01-06 14:29:41 +01:00
9fe576ae9e wdye: read out the whole terminfo database
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
Also update LICENSE years.
2025-01-06 11:59:46 +01:00
5c02778ff8 wdye: improve portability
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2025-01-06 10:14:49 +01:00
e40d56152d Add an Expect-like tool
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
This is to provide an Expect utility with a minimal dependency tree
for C-based projects.  It also addresses some Tcl Expect design issues,
as perceived by me.
2025-01-06 08:30:14 +01:00
21379d4c02 Update README
All checks were successful
Alpine 3.20 Success
OpenBSD 7.5 Success
2025-01-01 23:36:55 +01:00
9268fb8eba help2adoc: fix nawk
All checks were successful
Alpine 3.20 Success
2024-12-31 20:34:48 +01:00
b01df19b80 asciiman: have fewer "unexpected EOF" situations
Some checks failed
Alpine 3.20 Scripts failed
Easily caused by the new help2adoc.
2024-12-31 20:25:51 +01:00
09e635cf97 Add a --help/--version to AsciiDoc convertor
liberty is now self-contained, from opt_handler to manual page.
2024-12-31 20:25:51 +01:00
7560e8700e cmake-parser: improve portability 2024-12-31 06:47:31 +01:00
19 changed files with 2177 additions and 39 deletions

View File

@@ -10,7 +10,7 @@ if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
endif () endif ()
# Dependencies # Dependencies
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) set (CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
include (AddThreads) include (AddThreads)
find_package (PkgConfig REQUIRED) find_package (PkgConfig REQUIRED)
@@ -35,7 +35,7 @@ foreach (extra iconv rt)
endforeach () endforeach ()
# Build some unit tests # Build some unit tests
include_directories (${PROJECT_SOURCE_DIR}) include_directories ("${PROJECT_SOURCE_DIR}")
enable_testing () enable_testing ()
set (tests liberty proto xdg) set (tests liberty proto xdg)
@@ -57,7 +57,7 @@ endforeach ()
# --- Tools -------------------------------------------------------------------- # --- Tools --------------------------------------------------------------------
# Test the AsciiDoc manual page generator for a successful parse # Test the AsciiDoc manual page generator for a successful parse
set (ASCIIMAN ${PROJECT_SOURCE_DIR}/tools/asciiman.awk) set (ASCIIMAN "${PROJECT_SOURCE_DIR}/tools/asciiman.awk")
add_custom_command (OUTPUT libertyxdr.7 add_custom_command (OUTPUT libertyxdr.7
COMMAND env LC_ALL=C awk -f ${ASCIIMAN} COMMAND env LC_ALL=C awk -f ${ASCIIMAN}
"${PROJECT_SOURCE_DIR}/libertyxdr.adoc" > libertyxdr.7 "${PROJECT_SOURCE_DIR}/libertyxdr.adoc" > libertyxdr.7
@@ -65,10 +65,14 @@ add_custom_command (OUTPUT libertyxdr.7
COMMENT "Generating man page for libertyxdr" VERBATIM) COMMENT "Generating man page for libertyxdr" VERBATIM)
add_custom_target (docs ALL DEPENDS libertyxdr.7) add_custom_target (docs ALL DEPENDS libertyxdr.7)
# Test the --help/--version to AsciiDoc convertor
add_test (test-help2adoc
env LC_ALL=C "${PROJECT_SOURCE_DIR}/tests/help2adoc.sh")
# Test CMake script parsing # Test CMake script parsing
add_test (test-cmake-parser add_test (test-cmake-parser
env LC_ALL=C awk -f ${PROJECT_SOURCE_DIR}/tools/cmake-parser.awk env LC_ALL=C awk -f "${PROJECT_SOURCE_DIR}/tools/cmake-parser.awk"
-f ${PROJECT_SOURCE_DIR}/tools/cmake-dump.awk ${CMAKE_CURRENT_LIST_FILE}) -f "${PROJECT_SOURCE_DIR}/tools/cmake-dump.awk" ${CMAKE_CURRENT_LIST_FILE})
# Test protocol code generation # Test protocol code generation
set (lxdrgen_outputs) set (lxdrgen_outputs)
@@ -77,15 +81,15 @@ foreach (backend c cpp go mjs swift)
list (APPEND lxdrgen_outputs ${lxdrgen_base}.${backend}) list (APPEND lxdrgen_outputs ${lxdrgen_base}.${backend})
add_custom_command (OUTPUT ${lxdrgen_base}.${backend} add_custom_command (OUTPUT ${lxdrgen_base}.${backend}
COMMAND env LC_ALL=C awk COMMAND env LC_ALL=C awk
-f ${PROJECT_SOURCE_DIR}/tools/lxdrgen.awk -f "${PROJECT_SOURCE_DIR}/tools/lxdrgen.awk"
-f ${PROJECT_SOURCE_DIR}/tools/lxdrgen-${backend}.awk -f "${PROJECT_SOURCE_DIR}/tools/lxdrgen-${backend}.awk"
-v PrefixCamel=ProtoGen -v PrefixCamel=ProtoGen
${PROJECT_SOURCE_DIR}/tests/lxdrgen.lxdr "${PROJECT_SOURCE_DIR}/tests/lxdrgen.lxdr"
> ${lxdrgen_base}.${backend} > ${lxdrgen_base}.${backend}
DEPENDS DEPENDS
${PROJECT_SOURCE_DIR}/tools/lxdrgen.awk "${PROJECT_SOURCE_DIR}/tools/lxdrgen.awk"
${PROJECT_SOURCE_DIR}/tools/lxdrgen-${backend}.awk "${PROJECT_SOURCE_DIR}/tools/lxdrgen-${backend}.awk"
${PROJECT_SOURCE_DIR}/tests/lxdrgen.lxdr "${PROJECT_SOURCE_DIR}/tests/lxdrgen.lxdr"
COMMENT "Generating test protocol code (${backend})" VERBATIM) COMMENT "Generating test protocol code (${backend})" VERBATIM)
endforeach () endforeach ()
add_custom_target (test-lxdrgen-outputs ALL DEPENDS ${lxdrgen_outputs}) add_custom_target (test-lxdrgen-outputs ALL DEPENDS ${lxdrgen_outputs})

View File

@@ -1,4 +1,4 @@
Copyright (c) 2014 - 2024, Přemysl Eric Janouch <p@janouch.name> Copyright (c) 2014 - 2025, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted. purpose with or without fee is hereby granted.

View File

@@ -15,8 +15,8 @@ mess that are header files.
The API is intentionally unstable, which allows for easy refactoring. The API is intentionally unstable, which allows for easy refactoring.
All development is done on Linux, but other POSIX-compatible operating systems All development is done on Linux, but other POSIX-compatible operating systems
should be generally supported as well. They have an extremely low priority, should be generally supported as well. They have a lower priority, however,
however, and I'm not testing them at all, perhaps with the exception of macOS. and don't receive as much testing.
Tools Tools
----- -----
@@ -36,6 +36,10 @@ cmake-dump.awk::
This can be used in conjunction with the previous script to dump CMake This can be used in conjunction with the previous script to dump CMake
scripts in a normalized format for further processing. scripts in a normalized format for further processing.
help2adoc.awk::
Produces AsciiDoc manual pages from --version/--help output.
These can then be processed by _asciiman.awk_.
lxdrgen.awk:: lxdrgen.awk::
Protocol code generator for a variant of XDR, Protocol code generator for a variant of XDR,
which is link:libertyxdr.adoc[documented separately]. which is link:libertyxdr.adoc[documented separately].
@@ -64,6 +68,9 @@ lxdrgen-mjs.awk::
lxdrgen-swift.awk:: lxdrgen-swift.awk::
LibertyXDR backend for the Swift programming language. LibertyXDR backend for the Swift programming language.
wdye::
Compiled Lua-based Expect-like utility, intended purely for build checks.
Contributing and Support Contributing and Support
------------------------ ------------------------
Use https://git.janouch.name/p/liberty to report any bugs, request features, Use https://git.janouch.name/p/liberty to report any bugs, request features,

View File

@@ -1594,6 +1594,8 @@ mpd_client_parse_kv (char *line, char **value)
static void static void
mpd_client_update_poller (struct mpd_client *self) mpd_client_update_poller (struct mpd_client *self)
{ {
if (self->state != MPD_CONNECTED)
return;
poller_fd_set (&self->socket_event, poller_fd_set (&self->socket_event,
self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN); self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN);
} }

View File

@@ -86,24 +86,30 @@ xdg_xsettings_update (struct xdg_xsettings *self, Display *dpy)
// TODO: We're supposed to lock the server. // TODO: We're supposed to lock the server.
// TODO: We're supposed to trap X errors. // TODO: We're supposed to trap X errors.
char *selection = xstrdup_printf ("_XSETTINGS_S%d", DefaultScreen (dpy)); char *selection = xstrdup_printf ("_XSETTINGS_S%d", DefaultScreen (dpy));
Window owner Atom selection_atom = XInternAtom (dpy, selection, True);
= XGetSelectionOwner (dpy, XInternAtom (dpy, selection, True));
free (selection); free (selection);
if (!selection_atom)
return;
Window owner = XGetSelectionOwner (dpy, selection_atom);
if (!owner) if (!owner)
return; return;
Atom xsettings_atom = XInternAtom (dpy, "_XSETTINGS_SETTINGS", True);
if (!xsettings_atom)
return;
Atom actual_type = None; Atom actual_type = None;
int actual_format = 0; int actual_format = 0;
unsigned long nitems = 0, bytes_after = 0; unsigned long nitems = 0, bytes_after = 0;
unsigned char *buffer = NULL; unsigned char *buffer = NULL;
Atom xsettings = XInternAtom (dpy, "_XSETTINGS_SETTINGS", True);
int status = XGetWindowProperty (dpy, int status = XGetWindowProperty (dpy,
owner, owner,
xsettings, xsettings_atom,
0L, 0L,
LONG_MAX, LONG_MAX,
False, False,
xsettings, xsettings_atom,
&actual_type, &actual_type,
&actual_format, &actual_format,
&nitems, &nitems,
@@ -112,7 +118,7 @@ xdg_xsettings_update (struct xdg_xsettings *self, Display *dpy)
if (status != Success || !buffer) if (status != Success || !buffer)
return; return;
if (actual_type != xsettings if (actual_type != xsettings_atom
|| actual_format != 8 || actual_format != 8
|| nitems < 12) || nitems < 12)
goto fail; goto fail;

View File

@@ -1884,6 +1884,8 @@ x11_init (struct poller *poller, struct attrs *app_attrs, size_t app_attrs_len)
if (!(g_xui.dpy = XkbOpenDisplay if (!(g_xui.dpy = XkbOpenDisplay
(NULL, &g_xui.xkb_base_event_code, NULL, NULL, NULL, NULL))) (NULL, &g_xui.xkb_base_event_code, NULL, NULL, NULL, NULL)))
exit_fatal ("cannot open display"); exit_fatal ("cannot open display");
if (!XftInit (NULL))
print_warning ("Fontconfig initialization failed");
if (!XftDefaultHasRender (g_xui.dpy)) if (!XftDefaultHasRender (g_xui.dpy))
exit_fatal ("XRender is not supported"); exit_fatal ("XRender is not supported");
if (!(g_xui.x11_im = XOpenIM (g_xui.dpy, NULL, NULL, NULL))) if (!(g_xui.x11_im = XOpenIM (g_xui.dpy, NULL, NULL, NULL)))
@@ -1912,8 +1914,6 @@ x11_init (struct poller *poller, struct attrs *app_attrs, size_t app_attrs_len)
g_xui.x11_xsettings = xdg_xsettings_make (); g_xui.x11_xsettings = xdg_xsettings_make ();
xdg_xsettings_update (&g_xui.x11_xsettings, g_xui.dpy); xdg_xsettings_update (&g_xui.x11_xsettings, g_xui.dpy);
if (!FcInit ())
print_warning ("Fontconfig initialization failed");
if (!(g_xui.xft_fonts = x11_font_open (0))) if (!(g_xui.xft_fonts = x11_font_open (0)))
exit_fatal ("cannot open a font"); exit_fatal ("cannot open a font");

View File

@@ -1209,7 +1209,10 @@ async_make (struct async_manager *manager)
} }
/// Only allowed from the main thread once the job has been started but before /// Only allowed from the main thread once the job has been started but before
/// the results have been dispatched /// the results have been dispatched.
///
/// Note that it may in practice lead to memory leakage, although that's
/// an implementation issue: https://eissing.org/icing/posts/rip_pthread_cancel/
static void static void
async_cancel (struct async *self) async_cancel (struct async *self)
{ {

View File

@@ -93,10 +93,12 @@ and always-present field, which must be a tag *enum*:
case CAR: void; case CAR: void;
case LORRY: i8 axles; case LORRY: i8 axles;
case PLANE: i8 engines; case PLANE: i8 engines;
default: void;
}; };
All possible enumeration values must be named, and there is no *case* There is no *case* fall-through.
fall-through. Unless *default* is present, only the listed enumeration values are valid.
Any *default* must currently be empty.
Framing Framing
------- -------

View File

@@ -8,7 +8,7 @@ syn region libertyxdrBlockComment start=+/[*]+ end=+[*]/+
syn match libertyxdrComment "//.*" syn match libertyxdrComment "//.*"
syn match libertyxdrIdentifier "\<[[:alpha:]][[:alnum:]_]*\>" syn match libertyxdrIdentifier "\<[[:alpha:]][[:alnum:]_]*\>"
syn match libertyxdrNumber "\<0\>\|\(-\|\<\)[1-9][[:digit:]]*\>" syn match libertyxdrNumber "\<0\>\|\(-\|\<\)[1-9][[:digit:]]*\>"
syn keyword libertyxdrKeyword const enum struct union switch case syn keyword libertyxdrKeyword const enum struct union switch case default
syn keyword libertyxdrType bool u8 u16 u32 u64 i8 i16 i32 i64 string void syn keyword libertyxdrType bool u8 u16 u32 u64 i8 i16 i32 i64 string void
let b:current_syntax = "libertyxdr" let b:current_syntax = "libertyxdr"

214
tests/help2adoc.sh Executable file
View File

@@ -0,0 +1,214 @@
#!/bin/sh -e
# This test very exactly matches the output,
# but help2adoc is more or less feature-complete already.
self=$(realpath "$0")
help2adoc=$(realpath "$(dirname "$0")/../tools/help2adoc.awk")
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
test_oneline_help() {
cat <<END
Usage: $self [--brightness [+-]BRIGHTNESS] [--input NAME] [--restart]
END
}
test_oneline_version() {
cat <<'END'
eizoctl 1.0
END
}
test_oneline_out() {
cat <<'END'
eizoctl(1)
==========
:doctype: manpage
:manmanual: eizoctl Manual
:mansource: eizoctl 1.0
Name
----
eizoctl - manual page for eizoctl 1.0
Synopsis
--------
*eizoctl* [**--brightness** [+-]__BRIGHTNESS__] [**--input** __NAME__] [**--restart**]
END
}
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
test_simple_help() {
cat <<'END'
Usage: elksmart-comm [OPTION]... [COMMAND...]
Usage: elksmart-comm
Transmit or receive infrared commands.
-d, --debug elksmart-comm will run in debug mode
-f, --frequency HZ frequency (38000 Hz by default)
-n, --nec use the NEC transmission format
-h, --help display this help and exit
-V, --version output version information and exit
END
}
test_simple_version() {
cat <<'END'
elksmart-comm (usb-drivers) dev
END
}
test_simple_out() {
cat <<'END'
elksmart-comm(1)
================
:doctype: manpage
:manmanual: elksmart-comm Manual
:mansource: usb-drivers dev
Name
----
elksmart-comm - manual page for elksmart-comm dev
Synopsis
--------
*elksmart-comm* [__OPTION__]... [__COMMAND__...]
*elksmart-comm*
Description
-----------
Transmit or receive infrared commands.
*-d*, **--debug**::
**elksmart-comm** will run in debug mode
*-f*, **--frequency** __HZ__::
frequency (38000 Hz by default)
*-n*, **--nec**::
use the NEC transmission format
*-h*, **--help**::
display this help and exit
*-V*, **--version**::
output version information and exit
END
}
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
test_wild_help() {
cat <<'END'
Usage:
wild [option]… <command>… — Be wild
What's happening?
-f, --frequency hz-2-foo frequency to --foo at
--foo=bar
Foobar.
Boo far.
Subsection:
--help
--version
Oh my.
Major section:
And now for something completely different.
Very wild
END
}
test_wild_version() {
cat <<'END'
wild 1
Copies left and right.
END
}
test_wild_out() {
cat <<'END'
wild(1)
=======
:doctype: manpage
:manmanual: wild Manual
:mansource: wild 1
Name
----
wild - manual page for wild 1
Synopsis
--------
*wild* [__option__]... <__command__>...
Description
-----------
Be **wild**
What's happening?
*-f*, **--frequency** __hz-2-foo__::
frequency to **--foo** at
*--foo*=__bar__::
Foobar.
Boo far.
Subsection
~~~~~~~~~~
*--help*::
*--version*::
Oh my.
Major section
-------------
And now for something completely different.
Very wild
END
}
#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
run() {
echo "-- help2adoc/$1"
local selfquoted=$(echo "$self" | sed 's/\\/&&/g')
local output=$(TEST=$1 awk -f "$help2adoc" -v Target="$selfquoted")
local expect="$($1_out)"
if [ "$output" = "$expect" ]
then return
fi
echo "== Expected"
sed 's/^/ /' <<-END
$expect
END
echo "== Received"
sed 's/^/ /' <<-END
$output
END
exit 1
}
if [ -z "$TEST" ]
then
run test_oneline
run test_simple
run test_wild
echo "-- OK"
elif [ "$1" = "--help" ]
then ${TEST}_help
elif [ "$1" = "--version" ]
then ${TEST}_version
else
echo "Wrong usage"
exit 1
fi

View File

@@ -25,5 +25,7 @@ struct Struct {
union Onion switch (Enum tag) { union Onion switch (Enum tag) {
case NOTHING: case NOTHING:
void; void;
default:
void;
} o; } o;
}; };

View File

@@ -200,7 +200,7 @@ function inline(line) {
} }
# Returns 1 iff the left-over $0 should be processed further. # Returns 1 iff the left-over $0 should be processed further.
function process(firstline, posattrs, namedattrs) { function process(firstline, posattrs, namedattrs, ok) {
if (readattribute(firstline)) if (readattribute(firstline))
return 0 return 0
if (getline <= 0) { if (getline <= 0) {
@@ -281,9 +281,11 @@ function process(firstline, posattrs, namedattrs) {
while ($0) { while ($0) {
sub(/^[[:space:]]+/, "") sub(/^[[:space:]]+/, "")
sub(/^[+]$/, "") sub(/^[+]$/, "")
if (!process($0) && getline <= 0) if (!process($0) && (ok = getline) <= 0) {
fatal("unexpected EOF") if (ok < 0)
if (match($0, /^[[:space:]]*[*][[:space:]]+/)) fatal("getline failed")
$0 = ""
} else if (match($0, /^[[:space:]]*[*][[:space:]]+/))
break break
} }
print ".RE" print ".RE"
@@ -318,9 +320,11 @@ function process(firstline, posattrs, namedattrs) {
while ($0) { while ($0) {
sub(/^[[:space:]]+/, "") sub(/^[[:space:]]+/, "")
sub(/^[+]$/, "") sub(/^[+]$/, "")
if (!process($0) && getline <= 0) if (!process($0) && (ok = getline) <= 0) {
fatal("unexpected EOF") if (ok < 0)
if (match($0, /::$/)) fatal("getline failed")
$0 = ""
} else if (match($0, /::$/))
break break
} }
print ".RE" print ".RE"

View File

@@ -75,7 +75,7 @@ function line_ending() {
# it doesn't seem to be worth the effort. # it doesn't seem to be worth the effort.
function expand(s, v) { function expand(s, v) {
v = s v = s
while (match(v, /\\*[$](|ENV|CACHE)[{]/)) { while (match(v, /\\*[$](ENV|CACHE)?[{]/)) {
if (index(substr(v, RSTART), "$") % 2 != 0) { if (index(substr(v, RSTART), "$") % 2 != 0) {
warning("variable expansion is not supported: " s) warning("variable expansion is not supported: " s)
return s return s

234
tools/help2adoc.awk Normal file
View File

@@ -0,0 +1,234 @@
# help2adoc.awk: convert --version/--help to AsciiDoc manual pages
#
# Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD
#
# Usage: awk -f help2adoc.awk -v Target=cat
#
# This is not intended to produce great output, merely useful output,
# if only because there is no real standard of what the input should look like.
#
# The only target that needs to work is liberty's own opt_handler.
# The expected input format is roughly that of GNU utilites.
function fatal(message) {
print "// " message
print "fatal error: " message > "/dev/stderr"
exit 1
}
# The input model of this script is that function take the next line on $0,
# read further lines as necessary, and leave the next line in $0 again.
function readline( ok) {
if ((ok = (Command | getline)) < 0)
fatal("read error")
if (!ok)
exit
}
function emboldenoptions(line) {
# -N, --newer=DATE-OR-FILE, --after-date=DATE-OR-FILE
sub(/^-[^-=,[:space:]{[<]/, "*&*", line)
while (match(line, /[^-_[:alnum:]*'+]-[^-=,[:space:]{[<]/)) {
line = substr(line, 1, RSTART) \
"**" substr(line, RSTART + 1, RLENGTH - 1) "**" \
substr(line, RSTART + RLENGTH)
}
sub(/^--[-_[:alnum:]]+/, "*&*", line)
while (match(line, /[^-_[:alnum:]*'+]--[-_[:alnum:]]+/)) {
line = substr(line, 1, RSTART) \
"**" substr(line, RSTART + 1, RLENGTH - 1) "**" \
substr(line, RSTART + RLENGTH)
}
return line
}
function formatinline(line, programname, last, i) {
# Go the extra step of emboldening the program name at word boundaries.
programname = ProgramName
gsub(/[][\\.^$(){}|*+?]/, "\\\\&", programname)
if (match(line, "^" programname "[^-_[:alnum:]*'+/]")) {
line = "**" substr(line, RSTART, RLENGTH - 1) "**" \
substr(line, RSTART + RLENGTH - 1)
}
while (match(line, "[^-_[:alnum:]*'+/]" programname "[^-_[:alnum:]*'+/]")) {
line = substr(line, 1, RSTART) \
"**" substr(line, RSTART + 1, RLENGTH - 2) "**" \
substr(line, RSTART + RLENGTH - 1)
}
if (match(line, "[^-_[:alnum:]*'+/]" programname "$")) {
line = substr(line, 1, RSTART) \
"**" substr(line, RSTART + 1, RLENGTH - 1) "**"
}
return emboldenoptions(line)
}
function printusage(usage, description) {
gsub(/…/, "...", usage)
gsub(/—|/, "-", usage)
# --help output will more likely than not simply include argv[0],
# or perhaps program_invocation_short_name (not addressed here).
if (substr(usage, 1, length(Target) + 1) == Target " ")
usage = ProgramName substr(usage, length(Target) + 1)
# A lot of GNOME software includes the description here.
if (match(usage, / +- +/) && usage !~ / - [^[:alnum:]]/) {
description = substr(usage, RSTART + RLENGTH)
usage = substr(usage, 1, RSTART - 1)
}
while (match(usage, /[^-_[:alnum:]*'+.][[:alnum:]][-_[:alnum:]]+/)) {
usage = substr(usage, 1, RSTART) \
"__" substr(usage, RSTART + 1, RLENGTH - 1) "__" \
substr(usage, RSTART + RLENGTH)
}
sub(/^[^[:space:]]+/, "*&*", usage)
print emboldenoptions(usage)
print ""
if (description) {
flushsections()
print formatinline(description)
print ""
}
}
# We're going with Setext headers, because that's what asciiman.awk supports.
function printheader(text, underline) {
print text
gsub(/./, underline, text)
print text
}
BEGIN {
if (!Target)
fatal("missing Target")
TargetQuoted = Target
gsub(/'/, "'\\''", TargetQuoted)
TargetQuoted = "'" TargetQuoted "'"
# Remaining --version lines could be about copyright (GNU),
# or something else entirely.
Command = TargetQuoted " --version"
if ((Command | getline) > 0) {
# GNU --version output can place the package name in parentheses.
Package = $0
if (match($0, /[[:space:]][(][^)]*[)]/)) {
Package = substr($0, RSTART + 2, RLENGTH - 3) \
substr($0, RSTART + RLENGTH)
sub(/[[:space:]]+[(][^)]*[)]/, "")
}
Version = $0
sub(/[[:space:]]+[^[:space:]]+$/, "")
Name = $0
} else {
fatal("failed to get --version output")
}
if (Name !~ /[[:space:]]/)
ProgramName = Name
else if (match(Target, /[^\/]+$/))
ProgramName = substr(Target, RSTART, RLENGTH)
printheader(ProgramName "(1)", "=")
print ":doctype: manpage"
print ":manmanual: " Name " Manual"
print ":mansource: " Package
print ""
printheader("Name", "-")
print ProgramName " - manual page for " Version
print ""
close(Command)
Command = TargetQuoted " --help"
if ((Command | getline) <= 0)
fatal("failed to get --help output")
NextSection = "Description"
NextSubsection = ""
# The SYNOPSIS section is mandatory, so just put it there.
printheader("Synopsis", "-")
while (1) {
if (match($0, /^[Uu]sage:[[:space:]]*/)) {
if (($0 = substr($0, RSTART + RLENGTH)))
printusage($0)
} else if (match($0, /^[[:space:]]+/) && !/^[[:space:]]*-/) {
if (($0 = substr($0, RSTART + RLENGTH)))
printusage($0)
} else if ($0) {
break
}
readline()
}
while (1) {
if (match($0, /^[[:alpha:]][-[:alnum:][:space:]]+:$/)) {
# We don't flush sections here,
# so that we don't unnecessarily enforce DESCRIPTION first.
NextSection = substr($0, RSTART, RLENGTH - 1)
} else if (match($0, /^ [[:alpha:]][-[:alnum:][:space:]]+:$/)) {
flushsections()
NextSubsection = substr($0, RSTART + 1, RLENGTH - 2)
} else if (match($0, /^ +-/)) {
flushsections()
parseoption(substr($0, RSTART + RLENGTH - 1))
continue
} else if ($0) {
flushsections()
# That will be probably interpreted as a literal block.
if (!/^[[:space:]]/)
$0 = formatinline($0)
print
} else {
print
}
readline()
}
}
function flushsections() {
if (NextSection) {
print ""
printheader(NextSection, "-")
NextSection = ""
}
if (NextSubsection) {
print ""
printheader(NextSubsection, "~")
NextSubsection = ""
}
}
function parseoption(line, usage) {
# Often enough you will see it separated with only one space,
# which will simply not work for us.
if (match(line, /[[:space:]]{2,}/)) {
usage = substr(line, 1, RSTART - 1)
line = substr(line, RSTART + RLENGTH)
} else {
usage = line
line = ""
}
usage = emboldenoptions(usage)
while (match(usage, /[=<, ][[:alnum:]][-_[:alnum:]]*/)) {
usage = substr(usage, 1, RSTART) \
"__" substr(usage, RSTART + 1, RLENGTH - 1) "__" \
substr(usage, RSTART + RLENGTH)
}
print ""
print usage "::"
if (line)
print "\t" formatinline(line)
readline()
while (match($0, /^ +[^-[:space:]]|^ {7,}./)) {
print "\t" formatinline(substr($0, RSTART + RLENGTH - 1))
readline()
}
}

View File

@@ -1,6 +1,6 @@
# lxdrgen.awk: an XDR-derived code generator for network protocols. # lxdrgen.awk: an XDR-derived code generator for network protocols.
# #
# Copyright (c) 2022 - 2023, Přemysl Eric Janouch <p@janouch.name> # Copyright (c) 2022 - 2025, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
# #
# Usage: env LC_ALL=C awk -f lxdrgen.awk -f lxdrgen-{c,go,mjs}.awk \ # Usage: env LC_ALL=C awk -f lxdrgen.awk -f lxdrgen-{c,go,mjs}.awk \
@@ -218,7 +218,7 @@ function defstruct( name, d, cg) {
} }
function defunion( name, tag, tagtype, tagvalue, cg, scg, d, a, i, function defunion( name, tag, tagtype, tagvalue, cg, scg, d, a, i,
unseen, exhaustive) { unseen, defaulted, exhaustive) {
delete cg[0] delete cg[0]
delete scg[0] delete scg[0]
delete d[0] delete d[0]
@@ -249,9 +249,22 @@ function defunion( name, tag, tagtype, tagvalue, cg, scg, d, a, i,
if (!unseen[tagvalue]--) if (!unseen[tagvalue]--)
fatal("no such value or duplicate case: " tagtype "." tagvalue) fatal("no such value or duplicate case: " tagtype "." tagvalue)
codegen_struct_tag(tag, scg) codegen_struct_tag(tag, scg)
} else if (accept("default")) {
if (tagvalue)
codegen_union_struct(name, tagvalue, cg, scg)
expect(accept(":"))
if (defaulted)
fatal("duplicate default")
tagvalue = ""
defaulted = 1
} else if (tagvalue) { } else if (tagvalue) {
if (readfield(d)) if (readfield(d))
codegen_struct_field(d, scg) codegen_struct_field(d, scg)
} else if (defaulted) {
if (readfield(d))
fatal("default must not contain fields")
} else { } else {
fatal("union fields must fall under a case") fatal("union fields must fall under a case")
} }
@@ -259,11 +272,17 @@ function defunion( name, tag, tagtype, tagvalue, cg, scg, d, a, i,
if (tagvalue) if (tagvalue)
codegen_union_struct(name, tagvalue, cg, scg) codegen_union_struct(name, tagvalue, cg, scg)
# Unseen cases are simply not recognized/allowed. # Unseen cases are only recognized/allowed when default is present.
exhaustive = 1 exhaustive = 1
for (i in unseen) for (i in unseen)
if (i && unseen[i]) if (i && unseen[i]) {
exhaustive = 0 if (defaulted) {
codegen_struct_tag(tag, scg)
codegen_union_struct(name, i, cg, scg)
} else {
exhaustive = 0
}
}
Types[name] = "union" Types[name] = "union"
codegen_union(name, cg, exhaustive) codegen_union(name, cg, exhaustive)

42
tools/wdye/CMakeLists.txt Normal file
View File

@@ -0,0 +1,42 @@
cmake_minimum_required (VERSION 3.18)
project (wdye VERSION 1 DESCRIPTION "What did you expect?" LANGUAGES C)
set (CMAKE_C_STANDARD 99)
set (CMAKE_C_STANDARD_REQUIRED ON)
set (CMAKE_C_EXTENSIONS OFF)
# -Wunused-function is pretty annoying here, as everything is static
set (options -Wall -Wextra -Wno-unused-function)
add_compile_options ("$<$<CXX_COMPILER_ID:GNU>:${options}>")
add_compile_options ("$<$<CXX_COMPILER_ID:Clang>:${options}>")
set (CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/../../cmake")
find_package (Curses)
find_package (PkgConfig REQUIRED)
pkg_search_module (lua REQUIRED
lua53 lua5.3 lua-5.3 lua54 lua5.4 lua-5.4 lua>=5.3)
option (WITH_CURSES "Offer terminal sequences using Curses" "${CURSES_FOUND}")
# -liconv may or may not be a part of libc
find_path (iconv_INCLUDE_DIRS iconv.h)
include_directories (BEFORE "${PROJECT_BINARY_DIR}" ${iconv_INCLUDE_DIRS})
file (CONFIGURE OUTPUT "${PROJECT_BINARY_DIR}/config.h" CONTENT [[
#define PROGRAM_NAME "${PROJECT_NAME}"
#define PROGRAM_VERSION "${PROJECT_VERSION}"
#cmakedefine WITH_CURSES
]])
add_executable (wdye wdye.c)
target_include_directories (wdye PUBLIC ${lua_INCLUDE_DIRS})
target_link_directories (wdye PUBLIC ${lua_LIBRARY_DIRS})
target_link_libraries (wdye PUBLIC ${lua_LIBRARIES})
if (WITH_CURSES)
target_include_directories (wdye PUBLIC ${CURSES_INCLUDE_DIRS})
target_link_libraries (wdye PUBLIC ${CURSES_LIBRARIES})
endif ()
add_test (NAME wdye COMMAND wdye "${PROJECT_SOURCE_DIR}/test.lua")
include (CTest)

33
tools/wdye/test.lua Normal file
View File

@@ -0,0 +1,33 @@
for k, v in pairs(wdye) do _G[k] = v end
-- The terminal echoes back, we don't want to read the same stuff twice.
local cat = spawn {"sh", "-c", "cat > /dev/null", environ={TERM="xterm"}}
assert(cat, "failed to spawn process")
assert(cat.term.key_left, "bad terminfo")
cat:send("Hello\r")
local m = expect(cat:exact {"Hello\r", function (p) return p[0] end})
assert(m == "Hello\r", "exact match failed, or value expansion mismatch")
local t = table.pack(expect(timeout {.5, 42}))
assert(#t == 1 and t[1] == 42, "timeout match failed, or value mismatch")
cat:send("abc123\r")
expect(cat:regex {"A(.*)3", nocase=true, function (p)
assert(p[0] == "abc123", "wrong regex group #0")
assert(p[1] == "bc12", "wrong regex group #1")
end})
assert(not cat:wait (true), "process reports exiting early")
-- Send EOF (^D), test method chaining.
cat:send("Closing...\r"):send("\004")
local v = expect(cat:eof {true},
cat:default {.5, function (p) error "expected EOF, got a timeout" end})
assert(cat.pid > 0, "process has no ID")
local s1, exit, signal = cat:wait ()
assert(s1 == 0 and exit == 0 and not signal, "unexpected exit status")
assert(cat.pid < 0, "process still has an ID")
local s2 = cat:wait (true)
assert(s1 == s2, "exit status not remembered")

146
tools/wdye/wdye.adoc Normal file
View File

@@ -0,0 +1,146 @@
wdye(1)
=======
:doctype: manpage
:manmanual: wdye Manual
:mansource: wdye {release-version}
Name
----
wdye - what did you expect: Lua-based Expect tool
Synopsis
--------
*wdye* _program.lua_
Description
-----------
*wdye* executes a Lua script, providing an *expect*(1)-like API targeted
at application testing.
API
---
This list is logically ordered. Uppercase names represent object types.
wdye.spawn {file [, arg1, ...] [, environ=env]}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Creates a new pseudoterminal, spawns the given program in it,
and returns a _process_ object. When *file* doesn't contain slashes,
the program will be searched for in _PATH_.
The *env* map may be used to override environment variables, notably _TERM_.
Variables evaluating to _false_ will be removed from the environment.
The program's whole process group receives SIGKILL when the _process_
is garbage-collected, unless *wait* has collected the process group leader.
wdye.expect ([pattern1, ...])
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Waits until any pattern is ready, in order.
When no *timeout* (or *default*) patterns are included, one is added implicitly.
The function returns the matching _pattern_'s values, while replacing
any included functions with the results of their immediate evaluation,
passing the matching _pattern_ as their sole argument.
wdye.timeout {[timeout, ] [value1, ...]}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns a new timeout _pattern_. When no *timeout* is given, which is specified
in seconds, a default timeout value is assumed. Any further values
are remembered to be later processed by *expect*.
wdye.continue ()
~~~~~~~~~~~~~~~~
Raises a _nil_ error, which is interpreted by *expect* as a signal to restart
all processing.
PROCESS.buffer
~~~~~~~~~~~~~~
A string with the _process_' current read buffer contents.
PROCESS.pid
~~~~~~~~~~~
An integer with the _process_' process ID, or -1 if *wait* has collected it.
PROCESS.term
~~~~~~~~~~~~
A table with the _process_' *terminfo*(5) capabilities,
notably containing all **key_...** codes.
This functionality may not be enabled, then this table will always be empty.
PROCESS:send ([string, ...])
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Writes the given strings to the _process_' terminal slave,
and returns the _process_ for method chaining.
Beware of echoing and deadlocks, as only *expect* can read from the _process_,
and thus consume the terminal slave's output queue.
PROCESS:regex {pattern [, nocase=true] [, notransfer=true] [, value1, ...]}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns a new regular expression _pattern_. The *pattern* is a POSIX
Extended Regular Expression. Whether it can match NUL bytes depends on your
system C library.
When the *nocase* option is _true_, the expression will be matched
case-insensitively.
Unless the *notransfer* option is _true_, all data up until the end of the match
will be erased from the _process_' read buffer upon a successful match.
PROCESS:exact {literal [, nocase=true] [, notransfer=true] [, value1, ...]}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns a new literal string _pattern_. This behaves as if the *literal*
had its ERE special characters quoted, and was then passed to *regex*.
This _pattern_ can always match NUL bytes.
PROCESS:eof {[notransfer=true, ] [value1, ...]}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns a new end-of-file _pattern_, which matches the entire read buffer
contents once the child process closes the terminal.
PROCESS:wait ([nowait])
~~~~~~~~~~~~~~~~~~~~~~~
Waits for the program to terminate, and returns three values:
a combined status as used by `$?` in shells,
an exit status, and a termination signal number.
One of the latter two values will be _nil_, as appropriate.
When the *nowait* option is _true_, the function returns immediately.
If the process hasn't terminated yet, the function then returns no values.
PROCESS:default {[timeout, ] [notransfer=true, ] [value1, ...]}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns a new _pattern_ combining *wdye.timeout* with *eof*.
PATTERN.process
~~~~~~~~~~~~~~~
A reference to the _pattern_'s respective process, or _nil_.
PATTERN[group]
~~~~~~~~~~~~~~
For patterns that can match data, the zeroth group will be the whole matched
input sequence.
For *regex* patterns, positive groups relate to regular expression subgroups.
Missing groups evaluate to _nil_.
Example
-------
for k, v in pairs(wdye) do _G[k] = v end
local rot13 = spawn {"tr", "A-Za-z", "N-ZA-Mn-za-m", environ={TERM="dumb"}}
rot13:send "Hello\r"
expect(rot13:exact {"Uryyb\r"})
Environment
-----------
*WDYE_LOGGING*::
When this environment variable is present, *wdye* produces asciicast v2
files for every spawned program, in the current working directory.
Reporting bugs
--------------
Use https://git.janouch.name/p/liberty to report bugs, request features,
or submit pull requests.
See also
--------
*expect*(1), *terminfo*(5), *regex*(7)

1420
tools/wdye/wdye.c Normal file

File diff suppressed because it is too large Load Diff