Version 0.2.0: RDB_ICASE, Changelog, --version, angepasste Man-Pages

This commit is contained in:
wkh
2026-10-11 21:24:11 +02:00
parent b5b28798ac
commit 8f6dc852ff
18 changed files with 259 additions and 4 deletions
+32
View File
@@ -0,0 +1,32 @@
# Changelog
Alle wichtigen Änderungen stehen hier. Das Format folgt
[Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die Versionsnummern
folgen [Semantic Versioning](https://semver.org/lang/de/). Die Version steht in
`lib/rdb/version.rb`; `rdb --version` zeigt sie an.
## [Unveröffentlicht]
## [0.2.0] - 2026-10-11
### Hinzugefügt
- `RDB_ICASE=1` ignoriert Groß-/Kleinschreibung bei Spaltennamen. Ein exakter
Treffer gewinnt, mehrere Treffer sind ein Fehler; awk-Wörter und
Funktionsaufrufe bleiben unberührt.
- Man-Pages von `row`, `compute`, `column`, `sorttable`, `jointable`, `rename`,
`validate` und `rdb` mit dem Abschnitt „Case of Column Names“.
- `rdb man -g`: Man-Page mit groff formatiert, dazu `man/rdb.tmac` mit den
MWC-Makros (`.DM`, `.DE`, `.PC` ...).
- `rdb --version`, `CHANGELOG.md`.
### Geändert
- `column` schreibt den Kopf in der Schreibweise der Tabelle, wenn die Spalte
existiert.
- `demo/ledger` und `demo/chart` verwenden echte Tabs.
## [0.1.0] - 2026-10-11
### Hinzugefügt
- Erster Nachbau von /rdb in Ruby: ein Programm `bin/rdb` mit Symlinks für alle
dokumentierten Befehle (Relational, Text, Konvertierung, Schnellzugriff,
Berichte, Terminal, /act), 137 Man-Pages, Demo-Daten, 76 Tests, mise-Setup.
+16
View File
@@ -43,11 +43,19 @@ die meisten Befehle lesen beides.
`row`, `compute` und `validate` übergeben ihren Ausdruck an awk, nachdem sie `row`, `compute` und `validate` übergeben ihren Ausdruck an awk, nachdem sie
Spaltennamen durch `$N` ersetzt haben. Es gilt also awk-Syntax. Spaltennamen durch `$N` ersetzt haben. Es gilt also awk-Syntax.
Spaltennamen unterscheiden Groß- und Kleinschreibung. Mit `RDB_ICASE=1` wird
sie ignoriert (Details unter „Unterschiede zum Original“):
```
RDB_ICASE=1 compute 'wert = cost * amount' < inventory
```
## Hilfe ## Hilfe
``` ```
rdb alle Befehle rdb alle Befehle
rdb man search die Original-Man-Page als Text (auch Themen: backup, dbase, dashline ...) rdb man search die Original-Man-Page als Text (auch Themen: backup, dbase, dashline ...)
rdb --version Versionsnummer (Änderungen siehe CHANGELOG.md)
rdb man -g search dieselbe Seite mit groff formatiert (Beispiele sauber eingerückt) rdb man -g search dieselbe Seite mit groff formatiert (Beispiele sauber eingerückt)
whatis column Beschreibung und Syntax whatis column Beschreibung und Syntax
whatwill total Befehle, die zu einem Stichwort passen whatwill total Befehle, die zu einem Stichwort passen
@@ -104,6 +112,14 @@ er nicht mehr passt (außer mit `-a`). Beide halten die Indizes aktuell.
Handbuch sind als Tests enthalten (`test/rdb_test.rb`). Handbuch sind als Tests enthalten (`test/rdb_test.rb`).
- Die Indexdateien haben ein eigenes, einfaches Format (das Original ist nicht - Die Indexdateien haben ein eigenes, einfaches Format (das Original ist nicht
dokumentiert). Sie sind nicht mit alten /rdb-Dateien austauschbar. dokumentiert). Sie sind nicht mit alten /rdb-Dateien austauschbar.
- Spaltennamen unterscheiden wie im Original Groß- und Kleinschreibung. Mit
`RDB_ICASE=1` (z. B. in `mise.toml` unter `[env]`) wird sie ignoriert:
ein exakter Treffer gewinnt, mehrere ähnliche sind ein Fehler. In
`row`/`compute`/`validate` bleiben awk-Wörter und Funktionsaufrufe unberührt
(eine Spalte `Length` verdeckt also nicht `length()`). Die Man-Pages von
`row`, `compute`, `column`, `sorttable`, `jointable`, `rename`, `validate` und
`rdb` haben dazu den Abschnitt „Case of Column Names“; er ist die einzige
Ergänzung gegenüber den Original-Seiten.
- `jointable` braucht keine vorsortierten Tabellen. - `jointable` braucht keine vorsortierten Tabellen.
- `ve` ist ein zeilenorientierter Editor, kein Bildschirmeditor. - `ve` ist ein zeilenorientierter Editor, kein Bildschirmeditor.
- `soundex` nutzt den Standard-Algorithmus. Die Beispielwerte im Handbuch - `soundex` nutzt den Standard-Algorithmus. Die Beispielwerte im Handbuch
+1
View File
@@ -12,6 +12,7 @@ Signal.trap('PIPE', 'SYSTEM_DEFAULT')
if name == 'rdb' if name == 'rdb'
case args.first case args.first
when nil, '-h', '--help' then name = 'rdb.list' when nil, '-h', '--help' then name = 'rdb.list'
when '-v', '--version' then puts "rdb #{Rdb::VERSION}"; exit
when 'install' then name = 'rdb.install'; args.shift when 'install' then name = 'rdb.install'; args.shift
else name = args.shift else name = args.shift
end end
+16
View File
@@ -7,6 +7,7 @@ require 'open3'
require 'date' require 'date'
require 'tmpdir' require 'tmpdir'
require 'fileutils' require 'fileutils'
require_relative 'rdb/version'
module Rdb module Rdb
ROOT = File.expand_path('..', __dir__) ROOT = File.expand_path('..', __dir__)
@@ -16,6 +17,21 @@ module Rdb
COMMANDS = {} COMMANDS = {}
class << self class << self
# RDB_ICASE=1 makes column names match without regard to case. An exact
# match always wins; several case-insensitive matches are an error.
def icase?
!%w[0 no false].include?(ENV['RDB_ICASE'].to_s.downcase) && !ENV['RDB_ICASE'].to_s.empty?
end
def column_index(head, name)
exact = head.index(name)
return exact if exact || !icase?
hits = head.each_index.select { |i| head[i].casecmp?(name) }
die "column name '#{name}' is ambiguous: #{hits.map { |i| head[i] }.join(', ')}" if hits.size > 1
hits.first
end
def die(msg) def die(msg)
abort "#{File.basename($PROGRAM_NAME)}: #{msg}" abort "#{File.basename($PROGRAM_NAME)}: #{msg}"
end end
+3 -2
View File
@@ -15,8 +15,9 @@ Rdb.register('column', 'Display columns of a table in any order', 'column [ Colu
t = table_in t = table_in
next t.write if args.empty? next t.write if args.empty?
idx = args.map { |a| t.head.index(a) } idx = args.map { |a| Rdb.column_index(t.head, a) }
t.with(head: args, rows: t.rows.map { |r| idx.map { |i| i ? r[i] : '' } }).write head = args.each_with_index.map { |a, n| idx[n] ? t.head[idx[n]] : a }
t.with(head: head, rows: t.rows.map { |r| idx.map { |i| i ? r[i] : '' } }).write
end end
Rdb.alias_for('project', 'column') Rdb.alias_for('project', 'column')
+19
View File
@@ -40,6 +40,24 @@ module Rdb
# Replace column names in an awk expression with $N, leaving strings, # Replace column names in an awk expression with $N, leaving strings,
# regexes and other identifiers alone. # regexes and other identifiers alone.
AWK_WORDS = %w[BEGIN END NR NF FS OFS ORS RS FNR FILENAME SUBSEP RSTART RLENGTH ENVIRON ARGC ARGV
if else while for do in break continue next exit return function delete getline
print printf length substr index split sub gsub match sprintf sin cos atan2 exp log
sqrt int rand srand tolower toupper system].freeze
# With RDB_ICASE, a word that is not an awk keyword or function call may
# name a column in any case, provided that exactly one column fits.
def icase_column(names, expr, i)
return unless Rdb.icase?
word = expr[i..][/\A\w+/].to_s
return if word.empty? || AWK_WORDS.include?(word) || expr[i + word.size..].to_s.match?(/\A\s*\(/)
hits = names.select { |n, _| n.casecmp?(word) }
Rdb.die "column name '#{word}' is ambiguous: #{hits.map(&:first).join(', ')}" if hits.size > 1
hits.first
end
def to_awk(expr, head) def to_awk(expr, head)
names = head.each_with_index.sort_by { |n, _| -n.size } names = head.each_with_index.sort_by { |n, _| -n.size }
out = +'' out = +''
@@ -59,6 +77,7 @@ module Rdb
i = j + 1 i = j + 1
elsif c.match?(/[A-Za-z_#]/) && (i.zero? || !expr[i - 1].match?(/[\w$]/)) elsif c.match?(/[A-Za-z_#]/) && (i.zero? || !expr[i - 1].match?(/[\w$]/))
hit = names.find { |n, _| !n.empty? && expr[i, n.size] == n && !expr[i + n.size].to_s.match?(/\w/) } hit = names.find { |n, _| !n.empty? && expr[i, n.size] == n && !expr[i + n.size].to_s.match?(/\w/) }
hit ||= icase_column(names, expr, i)
if hit if hit
out << "$#{hit[1] + 1}" out << "$#{hit[1] + 1}"
i += hit[0].size i += hit[0].size
+1 -1
View File
@@ -53,7 +53,7 @@ module Rdb
end end
def column(name) def column(name)
@head.index(name) || Rdb.die("#{path}: no column named '#{name}'") Rdb.column_index(@head, name) || Rdb.die("#{path}: no column named '#{name}'")
end end
def row_at(offset) def row_at(offset)
+1 -1
View File
@@ -60,7 +60,7 @@ module Rdb
end end
def index_of(name) def index_of(name)
@head.index(name) || Rdb.die("no column named '#{name}'") Rdb.column_index(@head, name) || Rdb.die("no column named '#{name}'")
end end
def to_s def to_s
+5
View File
@@ -0,0 +1,5 @@
# frozen_string_literal: true
module Rdb
VERSION = '0.2.0'
end
+16
View File
@@ -113,6 +113,22 @@ Phone (111) 222-3333
Name Chiquita Banana Name Chiquita Banana
Phone 1234 Phone 1234
.DE .DE
.SH "Case of Column Names"
.I "(Ruby re-creation only.)"
Column names are case sensitive, as in the original
.BR /rdb .
If the environment variable
.B RDB_ICASE
is set to 1, upper and lower case are ignored when column names are looked up.
An exact match always wins;
if several columns differ only in case, the command stops with an
.I ambiguous
error.
Names that are also
.B awk
reserved words or are followed by
.B (
are never taken for columns.
.SH "See Also" .SH "See Also"
.B .B
project, project,
+16
View File
@@ -355,6 +355,22 @@ compute 'Value = sprintf ("%7.2f", ( Cost * Amount ))' < inventory
.DE .DE
.PP .PP
These commands will produce the first table under Examples, above. These commands will produce the first table under Examples, above.
.SH "Case of Column Names"
.I "(Ruby re-creation only.)"
Column names are case sensitive, as in the original
.BR /rdb .
If the environment variable
.B RDB_ICASE
is set to 1, upper and lower case are ignored when column names are looked up.
An exact match always wins;
if several columns differ only in case, the command stops with an
.I ambiguous
error.
Names that are also
.B awk
reserved words or are followed by
.B (
are never taken for columns.
.SH "See Also" .SH "See Also"
COHERENT Lexicon: COHERENT Lexicon:
.B .B
+16
View File
@@ -393,6 +393,22 @@ before
.BR chart . .BR chart .
It says that the standard input file It says that the standard input file
will be file 1 or on the left side of the table. will be file 1 or on the left side of the table.
.SH "Case of Column Names"
.I "(Ruby re-creation only.)"
Column names are case sensitive, as in the original
.BR /rdb .
If the environment variable
.B RDB_ICASE
is set to 1, upper and lower case are ignored when column names are looked up.
An exact match always wins;
if several columns differ only in case, the command stops with an
.I ambiguous
error.
Names that are also
.B awk
reserved words or are followed by
.B (
are never taken for columns.
.SH "See Also" .SH "See Also"
.B .B
column, column,
+16
View File
@@ -10,6 +10,22 @@ prints on the standard output all commands in directory
.BR $RDB/bin . .BR $RDB/bin .
This command is a shell script, which you should edit if its output This command is a shell script, which you should edit if its output
is not to your liking. is not to your liking.
.SH "Case of Column Names"
.I "(Ruby re-creation only.)"
Column names are case sensitive, as in the original
.BR /rdb .
If the environment variable
.B RDB_ICASE
is set to 1, upper and lower case are ignored when column names are looked up.
An exact match always wins;
if several columns differ only in case, the command stops with an
.I ambiguous
error.
Names that are also
.B awk
reserved words or are followed by
.B (
are never taken for columns.
.SH "See Also" .SH "See Also"
.B .B
act, act,
+16
View File
@@ -112,6 +112,22 @@ State Costa Rica
Zip 123456789 Zip 123456789
Phone 1234 Phone 1234
.DE .DE
.SH "Case of Column Names"
.I "(Ruby re-creation only.)"
Column names are case sensitive, as in the original
.BR /rdb .
If the environment variable
.B RDB_ICASE
is set to 1, upper and lower case are ignored when column names are looked up.
An exact match always wins;
if several columns differ only in case, the command stops with an
.I ambiguous
error.
Names that are also
.B awk
reserved words or are followed by
.B (
are never taken for columns.
.SH "See Also" .SH "See Also"
.B .B
column column
+16
View File
@@ -175,6 +175,22 @@ and
.BR compute , .BR compute ,
read the COHERENT manual's tutorial on read the COHERENT manual's tutorial on
.BR awk . .BR awk .
.SH "Case of Column Names"
.I "(Ruby re-creation only.)"
Column names are case sensitive, as in the original
.BR /rdb .
If the environment variable
.B RDB_ICASE
is set to 1, upper and lower case are ignored when column names are looked up.
An exact match always wins;
if several columns differ only in case, the command stops with an
.I ambiguous
error.
Names that are also
.B awk
reserved words or are followed by
.B (
are never taken for columns.
.SH "See Also" .SH "See Also"
.B .B
compute, compute,
+16
View File
@@ -165,6 +165,22 @@ People are often confused by
.B sort .B sort
because it does not behave the way one would expect. because it does not behave the way one would expect.
Study its entry in the COHERENT Lexicon. Study its entry in the COHERENT Lexicon.
.SH "Case of Column Names"
.I "(Ruby re-creation only.)"
Column names are case sensitive, as in the original
.BR /rdb .
If the environment variable
.B RDB_ICASE
is set to 1, upper and lower case are ignored when column names are looked up.
An exact match always wins;
if several columns differ only in case, the command stops with an
.I ambiguous
error.
Names that are also
.B awk
reserved words or are followed by
.B (
are never taken for columns.
.SH "See Also" .SH "See Also"
COHERENT Lexicon: COHERENT Lexicon:
.B .B
+16
View File
@@ -133,6 +133,22 @@ that is easy to find, such as
.PP .PP
If you only want to see the error lines, use the command If you only want to see the error lines, use the command
.BR row . .BR row .
.SH "Case of Column Names"
.I "(Ruby re-creation only.)"
Column names are case sensitive, as in the original
.BR /rdb .
If the environment variable
.B RDB_ICASE
is set to 1, upper and lower case are ignored when column names are looked up.
An exact match always wins;
if several columns differ only in case, the command stops with an
.I ambiguous
error.
Names that are also
.B awk
reserved words or are followed by
.B (
are never taken for columns.
.SH "See Also" .SH "See Also"
.SH "Logical Conditions" .SH "Logical Conditions"
.B .B
+37
View File
@@ -478,4 +478,41 @@ class ActTest < RdbCase
assert_includes out, 'Duplicate Short names in chart' assert_includes out, 'Duplicate Short names in chart'
assert st.success?, err assert st.success?, err
end end
ICASE = { 'RDB_ICASE' => '1' }.freeze
def test_column_names_are_case_sensitive_by_default
_o, err, st = run3('column', 'cost', stdin: File.read(File.join(@dir, 'inventory')))
assert st.success?, err
assert_equal %w[cost], _o.lines.first.chomp.split("\t")
end
def test_icase_column_and_sorttable
out, err, st = run3('column', 'COST', 'item', stdin: File.read(File.join(@dir, 'inventory')), env: ICASE)
assert st.success?, err
assert_equal %w[Cost Item], out.lines.first.chomp.split("\t")
out, = run3('sorttable', '-nr', 'cost', stdin: File.read(File.join(@dir, 'inventory')), env: ICASE)
assert_equal 'scales', data(out).first.last
end
def test_icase_compute_and_awk_words
src = File.read(File.join(@dir, 'inventory'))
out, err, st = run3('compute', 'value = sprintf("%5.2f", cost * AMOUNT)', stdin: src, env: ICASE)
assert st.success?, err
assert_equal '1312.75', data(out)[5][3].strip
# a column called Length must not swallow the length() function
t = "Length\tName\n------\t----\n3\tab\n"
out, err, st = run3('compute', 'Length = length(name)', stdin: t, env: ICASE)
assert st.success?, err
assert_equal [%w[2 ab]], data(out)
end
def test_icase_ambiguous_column_is_an_error
t = "Name\tname\n----\t----\na\tb\n"
_o, err, st = run3('column', 'NAME', stdin: t, env: ICASE)
refute st.success?
assert_match(/ambiguous/, err)
out, = run3('column', 'name', stdin: t, env: ICASE)
assert_equal [%w[b]], data(out)
end
end end