feat(rtl): LIST/DISPLAY TO FILE — text output redirection
Wire up TO FILE for both LIST and DISPLAY: __dbList grows a 9th
parameter cFile, opens it (truncating any prior content) when non-
empty, and writes the formatted rows there via fmt.Fprintln. Default
behavior (no TO FILE) still goes to stdout.
std.ch gets two new rules placed *before* the regular LIST/DISPLAY
patterns so they win when TO FILE is present:
LIST [<v,...>] TO FILE <(f)> [OFF] [FOR] [WHILE] [NEXT] ...
DISPLAY [<v,...>] TO FILE <(f)> [OFF] [FOR] [WHILE] [NEXT] ...
Open failure raises a clear *HbError ("LIST/DISPLAY TO FILE: cannot
create <path> — <syscall reason>") so callers know exactly what went
wrong instead of getting partial-or-empty output.
TO PRINTER stays rejected via __dbNotImpl — Five doesn't drive a
printer port. Test coverage: tests/std_ch/test_list_to_file.prg
exercises four shapes (full LIST, single-row DISPLAY, OFF + FOR with
explicit fields, and confirms TO PRINTER still raises). Wired into
the std.ch runner so the regression suite now stands at 14/14.
Gates green:
go test ./... : PASS
FiveSql2 SQL:1999 : 43/43
Harbour compat : 56/56
std.ch suite : 14/14
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -101,18 +101,25 @@
|
||||
/* --- console output ---
|
||||
LIST emits every record matching the filter; DISPLAY without ALL
|
||||
shows just the current record. Both share __dbList — lAll
|
||||
distinguishes them. TO PRINTER / TO FILE redirection is not yet
|
||||
implemented; the stub rules below surface a clear error rather
|
||||
than silently sending output to stdout when a printer/file was
|
||||
requested. Order matters: more specific rules first. */
|
||||
distinguishes them. TO FILE redirects to a freshly-truncated text
|
||||
file; TO PRINTER is rejected at PP-time (Five doesn't drive a
|
||||
printer port). Order matters: more specific rules first. */
|
||||
#command LIST [<v,...>] TO PRINTER [<*tail*>] => ;
|
||||
__dbNotImpl("LIST ... TO PRINTER")
|
||||
#command LIST [<v,...>] TO FILE <(f)> [<*tail*>] => ;
|
||||
__dbNotImpl("LIST ... TO FILE")
|
||||
#command DISPLAY [<v,...>] TO PRINTER [<*tail*>] => ;
|
||||
__dbNotImpl("DISPLAY ... TO PRINTER")
|
||||
#command DISPLAY [<v,...>] TO FILE <(f)> [<*tail*>] => ;
|
||||
__dbNotImpl("DISPLAY ... TO FILE")
|
||||
|
||||
#command LIST [<v,...>] TO FILE <(f)> [<off:OFF>] ;
|
||||
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
|
||||
[RECORD <rec>] [<rest:REST>] [ALL] => ;
|
||||
__dbList( <.off.>, { <{v}> }, .T., ;
|
||||
<{for}>, <{while}>, <next>, <rec>, <.rest.>, <(f)> )
|
||||
|
||||
#command DISPLAY [<v,...>] TO FILE <(f)> [<off:OFF>] ;
|
||||
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
|
||||
[RECORD <rec>] [<rest:REST>] [<all:ALL>] => ;
|
||||
__dbList( <.off.>, { <{v}> }, <.all.>, ;
|
||||
<{for}>, <{while}>, <next>, <rec>, <.rest.>, <(f)> )
|
||||
|
||||
#command LIST [<v,...>] [<off:OFF>] ;
|
||||
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
|
||||
|
||||
@@ -1214,20 +1214,21 @@ func rtlDbSort(t *hbrt.Thread) {
|
||||
}
|
||||
|
||||
// rtlDbList implements __dbList(lOff, aBlocks, lAll, bFor, bWhile,
|
||||
// nNext, nRec, lRest, lPrn, cFile) — output visible records to
|
||||
// stdout. aBlocks is an array of column-evaluation code blocks (one
|
||||
// per LIST / DISPLAY column expression). If aBlocks is empty or
|
||||
// contains only NIL placeholders, every field of the current
|
||||
// workarea is emitted.
|
||||
// nNext, nRec, lRest, cFile) — output visible records to stdout, or
|
||||
// to the named file when cFile is non-empty. aBlocks is an array of
|
||||
// column-evaluation code blocks (one per LIST / DISPLAY column
|
||||
// expression). If aBlocks is empty or contains only NIL placeholders,
|
||||
// every field of the current workarea is emitted.
|
||||
//
|
||||
// Used by both `LIST [<v,...>]` and `DISPLAY [<v,...>]` in std.ch.
|
||||
// lAll distinguishes them: LIST always passes .T. (all matching
|
||||
// records); DISPLAY passes .T. only for `DISPLAY ALL`, otherwise .F.
|
||||
// (just the current record).
|
||||
//
|
||||
// TO PRINTER / TO FILE redirection (lPrn / cFile) is accepted but
|
||||
// not yet implemented — both paths still write to stdout. OFF (lOff)
|
||||
// suppresses the record-number prefix.
|
||||
// TO FILE <(f)> redirects output into a freshly-truncated text file
|
||||
// (one record per line, fields space-separated). TO PRINTER is
|
||||
// rejected at PP-time via __dbNotImpl — Five doesn't drive a
|
||||
// printer port. OFF (lOff) suppresses the record-number prefix.
|
||||
func rtlDbList(t *hbrt.Thread) {
|
||||
nParams := t.ParamCount()
|
||||
t.Frame(nParams, 0)
|
||||
@@ -1298,6 +1299,28 @@ func rtlDbList(t *hbrt.Thread) {
|
||||
srcArea.GoTop()
|
||||
}
|
||||
|
||||
// param 9: cFile — when non-empty, redirect output into the named
|
||||
// text file. The previous file is truncated. We deliberately keep
|
||||
// the file open across the loop so the OS doesn't see N opens for
|
||||
// N rows; close on exit. On open failure: fall back to stdout
|
||||
// rather than producing partial output to nowhere.
|
||||
var sink interface {
|
||||
Write([]byte) (int, error)
|
||||
} = os.Stdout
|
||||
if nParams >= 9 && t.Local(9).IsString() {
|
||||
if cFile := strings.TrimSpace(t.Local(9).AsString()); cFile != "" {
|
||||
f, err := os.Create(cFile)
|
||||
if err != nil {
|
||||
panic(&hbrt.HbError{
|
||||
Description: "LIST/DISPLAY TO FILE: cannot create " + cFile + " — " + err.Error(),
|
||||
SubSystem: "BASE",
|
||||
})
|
||||
}
|
||||
defer f.Close()
|
||||
sink = f
|
||||
}
|
||||
}
|
||||
|
||||
nFields := srcArea.FieldCount()
|
||||
scanned := 0
|
||||
for !srcArea.EOF() {
|
||||
@@ -1344,8 +1367,10 @@ func rtlDbList(t *hbrt.Thread) {
|
||||
}
|
||||
// Newline after the row, not before — avoids the spurious
|
||||
// leading blank line at the top of the listing. `\n` only;
|
||||
// terminals handle CR conversion themselves.
|
||||
fmt.Println(strings.Join(parts, " "))
|
||||
// terminals handle CR conversion themselves. Goes to the
|
||||
// chosen sink (stdout by default, the file when TO FILE
|
||||
// was used).
|
||||
fmt.Fprintln(sink, strings.Join(parts, " "))
|
||||
}
|
||||
srcArea.Skip(1)
|
||||
scanned++
|
||||
|
||||
@@ -23,6 +23,7 @@ TESTS=(
|
||||
test_copy
|
||||
test_sort
|
||||
test_list
|
||||
test_list_to_file
|
||||
test_total
|
||||
test_join
|
||||
test_update
|
||||
|
||||
73
tests/std_ch/test_list_to_file.prg
Normal file
73
tests/std_ch/test_list_to_file.prg
Normal file
@@ -0,0 +1,73 @@
|
||||
/* LIST / DISPLAY TO FILE — text output redirected to a file. */
|
||||
|
||||
PROCEDURE Main()
|
||||
LOCAL aStruct, cBuf, e
|
||||
|
||||
FErase("p.dbf")
|
||||
FErase("out.txt")
|
||||
|
||||
aStruct := { ;
|
||||
{ "ID", "N", 4, 0 }, ;
|
||||
{ "NAME", "C", 10, 0 }, ;
|
||||
{ "AGE", "N", 3, 0 } }
|
||||
dbCreate("p.dbf", aStruct)
|
||||
USE p.dbf NEW EXCLUSIVE ALIAS p
|
||||
dbAppend(); FieldPut(1,1); FieldPut(2,"Alice"); FieldPut(3,18)
|
||||
dbAppend(); FieldPut(1,2); FieldPut(2,"Bob"); FieldPut(3,25)
|
||||
dbAppend(); FieldPut(1,3); FieldPut(2,"Carol"); FieldPut(3,30)
|
||||
dbCommit()
|
||||
|
||||
/* 1. LIST TO FILE — full table */
|
||||
dbGoTop()
|
||||
LIST TO FILE out.txt
|
||||
|
||||
cBuf := MemoRead("out.txt")
|
||||
? "1. file size:", Len(cBuf), "bytes (expect > 0)"
|
||||
IF Len(cBuf) == 0
|
||||
? "FAIL: empty output file"
|
||||
RETURN
|
||||
ENDIF
|
||||
? "1. file content:"
|
||||
? cBuf
|
||||
|
||||
/* 2. DISPLAY TO FILE — single record */
|
||||
FErase("out.txt")
|
||||
dbGoto(2)
|
||||
DISPLAY TO FILE out.txt
|
||||
|
||||
cBuf := MemoRead("out.txt")
|
||||
? "2. DISPLAY single-row file:"
|
||||
? cBuf
|
||||
IF !("Bob" $ cBuf)
|
||||
? "FAIL: Bob row missing"
|
||||
RETURN
|
||||
ENDIF
|
||||
|
||||
/* 3. LIST TO FILE with OFF + FOR — std.ch pattern order is
|
||||
`[<off:OFF>] [FOR <for>]`, same as Harbour. */
|
||||
FErase("out.txt")
|
||||
dbGoTop()
|
||||
LIST p->id, p->name TO FILE out.txt OFF FOR p->age >= 25
|
||||
|
||||
cBuf := MemoRead("out.txt")
|
||||
? "3. selective+OFF file:"
|
||||
? cBuf
|
||||
IF "Alice" $ cBuf
|
||||
? "FAIL: Alice (age 18) shouldn't be in FOR age>=25 output"
|
||||
RETURN
|
||||
ENDIF
|
||||
|
||||
/* 4. TO PRINTER — should still reject */
|
||||
e := .F.
|
||||
BEGIN SEQUENCE
|
||||
LIST TO PRINTER
|
||||
RECOVER
|
||||
e := .T.
|
||||
END SEQUENCE
|
||||
? "4. TO PRINTER rejected:", e, "(expect .T.)"
|
||||
|
||||
dbCloseArea()
|
||||
FErase("p.dbf")
|
||||
FErase("out.txt")
|
||||
? "DONE"
|
||||
RETURN
|
||||
Reference in New Issue
Block a user