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.
This commit is contained in:
Přemysl Eric Janouch 2025-01-02 23:29:50 +01:00
parent 21379d4c02
commit e40d56152d
Signed by: p
GPG Key ID: A0420B94F92B9493
5 changed files with 1423 additions and 0 deletions

View File

@ -68,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,

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

@ -0,0 +1,39 @@
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}")
include_directories ("${PROJECT_BINARY_DIR}")
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 simple COMMAND wdye "${PROJECT_SOURCE_DIR}/test.lua")
include (CTest)

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

@ -0,0 +1,24 @@
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})
-- 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})

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

@ -0,0 +1,126 @@
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.
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.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: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"})
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)

1231
tools/wdye/wdye.c Normal file

File diff suppressed because it is too large Load Diff