Fehler behoben: Umlaute, Latin-1, Fehlermeldungen, atomares Schreiben
Spaltennamen mit Umlauten in Ausdrücken, Latin-1-Dateien als UTF-8, Fehler als eine Zeile (RDB_DEBUG=1 für Backtrace), Werte mit Tab oder Zeilenumbruch abgelehnt, Tabellen und Indexe über Temp-Datei und rename, update.rdb gibt den Satz bei Editorfehler zurück, sorttable -r stabil.
This commit is contained in:
+10
-1
@@ -12,6 +12,7 @@ folgen [Semantic Versioning](https://semver.org/lang/de/). Die Version steht in
|
|||||||
- `demo/handbuch/`: die Beispiele aus dem Lehrbuchteil (Kapitel 2, 3, 6, 7, 10, 11, 15) mit ihren Tabellen. `beispiele.txt` hält Befehl und gedruckte Ausgabe, `test/handbuch_test.rb` vergleicht sie; `mise run test` führt ihn mit aus. Druckfehler des Buchs und Stellen, an denen wir abweichen, sind dort vermerkt.
|
- `demo/handbuch/`: die Beispiele aus dem Lehrbuchteil (Kapitel 2, 3, 6, 7, 10, 11, 15) mit ihren Tabellen. `beispiele.txt` hält Befehl und gedruckte Ausgabe, `test/handbuch_test.rb` vergleicht sie; `mise run test` führt ihn mit aus. Druckfehler des Buchs und Stellen, an denen wir abweichen, sind dort vermerkt.
|
||||||
|
|
||||||
### Geändert
|
### Geändert
|
||||||
|
- `testall` und die Tests setzen `EDITOR=true`, damit kein echter Editor startet.
|
||||||
- `search` und `seek` ohne Schlüsselspalte nehmen die Spaltennamen aus dem Kopf einer Schlüsseltabelle auf der Standardeingabe (`search -mh mailtable < name`, Handbuch S. 105). Vorher galt immer die erste Spalte der Datentabelle.
|
- `search` und `seek` ohne Schlüsselspalte nehmen die Spaltennamen aus dem Kopf einer Schlüsseltabelle auf der Standardeingabe (`search -mh mailtable < name`, Handbuch S. 105). Vorher galt immer die erste Spalte der Datentabelle.
|
||||||
- `gregorian -u` schreibt den Monat ohne führende Null (`6/01/89`), wie im Handbuch.
|
- `gregorian -u` schreibt den Monat ohne führende Null (`6/01/89`), wie im Handbuch.
|
||||||
- `mean` schreibt einen ganzzahligen Mittelwert ohne `.00` (`4786`, Handbuch S. 26).
|
- `mean` schreibt einen ganzzahligen Mittelwert ohne `.00` (`4786`, Handbuch S. 26).
|
||||||
@@ -23,7 +24,15 @@ folgen [Semantic Versioning](https://semver.org/lang/de/). Die Version steht in
|
|||||||
- `vilock`: die Sperre wird atomar über einen Hardlink genommen (vorher `exist?` und `rename` mit Lücke dazwischen).
|
- `vilock`: die Sperre wird atomar über einen Hardlink genommen (vorher `exist?` und `rename` mit Lücke dazwischen).
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
- `EDITOR` mit Argumenten (`nvim -f`) in `vilock` und `update.rdb` wurde als ein Programmname behandelt; der Editor startete nie.
|
- `EDITOR` mit Argumenten (`nvim -f`) in `vilock` und `update.rdb` wurde als ein Programmname behandelt; der Editor startete nie. Scheitert der Editor, meldet `update.rdb` das, statt „record replaced“ zu schreiben.
|
||||||
|
- `update.rdb e`: Der Satz blieb leer, wenn der Editor scheiterte oder abgebrochen wurde. Er wird jetzt in jedem Fall zurückgeschrieben.
|
||||||
|
- Spaltennamen mit Umlauten (`row 'Über > 3'`) scheiterten mit einem awk-Fehler. Der Ausdrucksumsetzer kennt jetzt Unicode-Buchstaben.
|
||||||
|
- Dateien, die kein UTF-8 sind (Latin-1 aus COHERENT-Zeiten), brachen mit `invalid byte sequence` ab. Sie werden als Latin-1 gelesen und als UTF-8 ausgegeben.
|
||||||
|
- Fehler wie ungültige Regex, ungültiges Datum oder Ein-/Ausgabefehler endeten mit einem Ruby-Backtrace. Jetzt eine Zeile; `RDB_DEBUG=1` zeigt den Backtrace. Ein geschlossenes Pipe-Ende (`| head`) und Ctrl-C beenden still.
|
||||||
|
- `append`, `enter`, `replace` und `ve` schrieben Werte mit Tab oder Zeilenumbruch und machten die Tabelle kaputt. Sie lehnen solche Werte jetzt ab.
|
||||||
|
- Tabellen, Indexdateien und `substitute` werden über eine temporäre Datei mit `rename` geschrieben; ein Abbruch lässt die alte Datei statt einer halben neuen.
|
||||||
|
- `index -mb` und `-mi` sortieren nach dem Schlüssel ohne umgebende Leerzeichen, so wie `search` vergleicht.
|
||||||
|
- `sorttable -r` kehrte auch die Reihenfolge gleicher Schlüssel um; sie bleibt jetzt erhalten.
|
||||||
- Spaltennamen, die `justify` mit Leerzeichen aufgefüllt hat, wurden nicht mehr gefunden: `column Name` lieferte eine leere Spalte, `index` brach ab. Leerraum um die Namen im Kopf zählt jetzt nicht mehr.
|
- Spaltennamen, die `justify` mit Leerzeichen aufgefüllt hat, wurden nicht mehr gefunden: `column Name` lieferte eine leere Spalte, `index` brach ab. Leerraum um die Namen im Kopf zählt jetzt nicht mehr.
|
||||||
|
|
||||||
## [0.7.0] - 2026-10-12
|
## [0.7.0] - 2026-10-12
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ sind nach dem Handbuch neu geschrieben.
|
|||||||
mise trust # einmal nach dem Clone: mise.toml freigeben
|
mise trust # einmal nach dem Clone: mise.toml freigeben
|
||||||
mise install # Ruby 3.4
|
mise install # Ruby 3.4
|
||||||
mise run install # legt die Symlinks row, column, total ... in bin/ an
|
mise run install # legt die Symlinks row, column, total ... in bin/ an
|
||||||
mise run test # 114 Tests und die Beispiele aus dem Handbuch
|
mise run test # 126 Tests und die Beispiele aus dem Handbuch
|
||||||
```
|
```
|
||||||
|
|
||||||
Ohne `mise trust` brechen in einem frisch geklonten Ordner alle `mise`-Befehle ab.
|
Ohne `mise trust` brechen in einem frisch geklonten Ordner alle `mise`-Befehle ab.
|
||||||
@@ -245,6 +245,9 @@ er nicht mehr passt (außer mit `-a`). Beide halten die Indizes aktuell.
|
|||||||
- Die Indexdateien sind dem Original nachgebaut, aber nicht gleich (Handbuch S. 103–108):
|
- Die Indexdateien sind dem Original nachgebaut, aber nicht gleich (Handbuch S. 103–108):
|
||||||
die Hashtabelle `.h` hat 2n statt 2n+1 Plätze, und in `.i` steht der Schlüssel vor
|
die Hashtabelle `.h` hat 2n statt 2n+1 Plätze, und in `.i` steht der Schlüssel vor
|
||||||
dem Offset statt dahinter. Alte /rdb-Indizes muss man neu bauen.
|
dem Offset statt dahinter. Alte /rdb-Indizes muss man neu bauen.
|
||||||
|
- Dateien, die kein UTF-8 sind, liest `rdb` als Latin-1 und schreibt sie als UTF-8. Fehler
|
||||||
|
erscheinen als eine Zeile; mit `RDB_DEBUG=1` kommt der Ruby-Backtrace. Werte mit Tab oder
|
||||||
|
Zeilenumbruch lehnen `append`, `enter`, `replace` und `ve` ab.
|
||||||
- `lock`/`unlock` legen ihre Sperrdateien in `/tmp/rdb-locks-<UID>/` ab (Modus 0700, `flock`),
|
- `lock`/`unlock` legen ihre Sperrdateien in `/tmp/rdb-locks-<UID>/` ab (Modus 0700, `flock`),
|
||||||
nicht als `/tmp/L<Tabelle>` wie in den Man-Pages. Eine Sperre eines abgestürzten Prozesses
|
nicht als `/tmp/L<Tabelle>` wie in den Man-Pages. Eine Sperre eines abgestürzten Prozesses
|
||||||
wird nicht erkannt; `unlock` hebt sie auf.
|
wird nicht erkannt; `unlock` hebt sie auf.
|
||||||
|
|||||||
+13
-3
@@ -27,7 +27,7 @@ Stand: 2026-10-30, Version 0.7.0. Die 114 Tests und der Handbuch-Test liefen gr
|
|||||||
Race zwischen `File.exist?` und `rename`; nach `kill -9` bleibt `LOCK…` liegen.
|
Race zwischen `File.exist?` und `rename`; nach `kill -9` bleibt `LOCK…` liegen.
|
||||||
**Behoben:** atomare Sperre über `File.link`. Offen: nach `kill -9` bleibt `LOCK…` liegen.
|
**Behoben:** atomare Sperre über `File.link`. Offen: nach `kill -9` bleibt `LOCK…` liegen.
|
||||||
|
|
||||||
## Fehler
|
## Fehler (1–8 behoben, 9 teilweise)
|
||||||
|
|
||||||
1. **Umlaute in Spaltennamen (bestätigt).** `to_awk` erkennt nur `[A-Za-z_#]`
|
1. **Umlaute in Spaltennamen (bestätigt).** `to_awk` erkennt nur `[A-Za-z_#]`
|
||||||
und ASCII-`\w`. `row 'Über > 3'` scheitert mit awk-Fehler. Relevant für die
|
und ASCII-`\w`. `row 'Über > 3'` scheitert mit awk-Fehler. Relevant für die
|
||||||
@@ -62,6 +62,16 @@ Stand: 2026-10-30, Version 0.7.0. Die 114 Tests und der Handbuch-Test liefen gr
|
|||||||
ignoriert; `total`/`mean` lehnen `1e3` ab; eine Zeile aus lauter leeren
|
ignoriert; `total`/`mean` lehnen `1e3` ab; eine Zeile aus lauter leeren
|
||||||
Zellen gilt als gelöscht ([fast.rb:21](../lib/rdb/fast.rb)).
|
Zellen gilt als gelöscht ([fast.rb:21](../lib/rdb/fast.rb)).
|
||||||
|
|
||||||
|
**Stand der Behebung:** 1 (Umlaute: Unicode-fähiger Umsetzer), 2 (Latin-1 wird als UTF-8
|
||||||
|
gelesen), 3 (Fehler als eine Zeile, `RDB_DEBUG=1` für den Backtrace), 4 (`EDITOR` mit
|
||||||
|
Argumenten, Editorfehler wird gemeldet), 5 (Satz wird in jedem Fall zurückgeschrieben),
|
||||||
|
6 (`Rdb.check_cells!`), 7 (`Rdb.write_file`: temporäre Datei plus `rename`) und 8
|
||||||
|
(Sortierschlüssel ohne Leerzeichen) sind behoben, jeweils mit Test.
|
||||||
|
Bei 9 ist nur `sorttable -r` behoben. Bewusst offen: `-nrf` gilt global (so beschreibt es
|
||||||
|
das Handbuch), `jointable -n` ist unklar spezifiziert, `1e3` als Zahl und die Zeile aus
|
||||||
|
leeren Zellen als „gelöscht“ sind Eigenheiten des Formats. Bei 8 bleibt `-x` (Faltung) beim
|
||||||
|
Index ohne Entsprechung in der Suche offen.
|
||||||
|
|
||||||
## Verbesserungspotential
|
## Verbesserungspotential
|
||||||
|
|
||||||
1. **Indexe sind nicht schnell.** Jede Suche liest Tabelle und Indexdatei
|
1. **Indexe sind nicht schnell.** Jede Suche liest Tabelle und Indexdatei
|
||||||
@@ -88,5 +98,5 @@ Stand: 2026-10-30, Version 0.7.0. Die 114 Tests und der Handbuch-Test liefen gr
|
|||||||
|
|
||||||
## Empfohlene Reihenfolge
|
## Empfohlene Reihenfolge
|
||||||
|
|
||||||
Die Schwachstellen sind erledigt. Weiter mit: Umlaute (Fehler 1), Encoding (2), Top-Level-Rescue (3), `report`-Einschleusung
|
Schwachstellen und Fehler sind erledigt. Weiter mit: Hash-Funktion und
|
||||||
(Schwachstelle 1); danach Hash-Funktion und Index-Lesezugriffe.
|
Index-Lesezugriffe (Verbesserung 1 und 2), dann Float-Geld.
|
||||||
|
|||||||
+29
@@ -46,9 +46,38 @@ module Rdb
|
|||||||
COMMANDS[name] = Command.new(name, cmd.desc, cmd.syntax.sub(target, name), cmd.block)
|
COMMANDS[name] = Command.new(name, cmd.desc, cmd.syntax.sub(target, name), cmd.block)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Write a file all at once: a temporary file next to it, then rename, so
|
||||||
|
# an interrupted run leaves the old file rather than half a new one.
|
||||||
|
def write_file(path, data)
|
||||||
|
path = File.realpath(path) if File.symlink?(path)
|
||||||
|
tmp = "#{path}.tmp#{Process.pid}"
|
||||||
|
File.binwrite(tmp, data)
|
||||||
|
File.chmod(File.stat(path).mode & 0o7777, tmp) if File.exist?(path)
|
||||||
|
File.rename(tmp, path)
|
||||||
|
ensure
|
||||||
|
File.delete(tmp) if tmp && File.exist?(tmp)
|
||||||
|
end
|
||||||
|
|
||||||
|
# A value is one cell of a tab-separated line: no tab, no line break.
|
||||||
|
def check_cells!(cells)
|
||||||
|
bad = cells.find { |c| c.to_s.match?(/[\t\r\n]/) }
|
||||||
|
die "value #{bad.to_s.inspect} contains a tab or line break" if bad
|
||||||
|
end
|
||||||
|
|
||||||
|
# Errors reach the user as one line; RDB_DEBUG=1 keeps the backtrace.
|
||||||
def run(name, args)
|
def run(name, args)
|
||||||
cmd = COMMANDS[name] or die("unknown command '#{name}'")
|
cmd = COMMANDS[name] or die("unknown command '#{name}'")
|
||||||
Env.new(name).instance_exec(args.dup, &cmd.block)
|
Env.new(name).instance_exec(args.dup, &cmd.block)
|
||||||
|
rescue Errno::EPIPE
|
||||||
|
exit 0
|
||||||
|
rescue Interrupt
|
||||||
|
exit 130
|
||||||
|
rescue SystemExit
|
||||||
|
raise
|
||||||
|
rescue StandardError => e
|
||||||
|
raise if ENV['RDB_DEBUG'].to_s.match?(/\A(1|yes|true)\z/i)
|
||||||
|
|
||||||
|
die("#{e.message} (#{e.class}; RDB_DEBUG=1 shows where)")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+25
-15
@@ -259,7 +259,7 @@ Rdb.register('vilock', 'Lock a table before editing, unlock afterward', 'vilock
|
|||||||
end
|
end
|
||||||
File.unlink(file)
|
File.unlink(file)
|
||||||
begin
|
begin
|
||||||
system(*Shellwords.split(ENV['EDITOR'] || 'vi'), locked)
|
system(*Shellwords.split(ENV['EDITOR'] || 'vi'), locked) or warn "vilock: editor #{ENV['EDITOR'] || 'vi'} failed"
|
||||||
ensure
|
ensure
|
||||||
File.rename(locked, file)
|
File.rename(locked, file)
|
||||||
end
|
end
|
||||||
@@ -276,7 +276,7 @@ def edit_cells(head, cells)
|
|||||||
Tempfile.create('rdb-edit') do |f|
|
Tempfile.create('rdb-edit') do |f|
|
||||||
f.write Rdb::Table.new(head, [cells], list: true).to_s
|
f.write Rdb::Table.new(head, [cells], list: true).to_s
|
||||||
f.flush
|
f.flush
|
||||||
system(*Shellwords.split(ENV['EDITOR'] || 'vi'), f.path)
|
system(*Shellwords.split(ENV['EDITOR'] || 'vi'), f.path) or Rdb.die("editor #{ENV['EDITOR'] || 'vi'} failed")
|
||||||
t = Rdb::Table.parse(File.read(f.path))
|
t = Rdb::Table.parse(File.read(f.path))
|
||||||
return nil if t.rows.empty?
|
return nil if t.rows.empty?
|
||||||
|
|
||||||
@@ -319,20 +319,27 @@ Rdb.register('update.rdb', 'Display and edit records in any sized file',
|
|||||||
|
|
||||||
r = rows[cur]
|
r = rows[cur]
|
||||||
cells = r.cells
|
cells = r.cells
|
||||||
|
# Unless -l, the record is blanked while it is edited (that is the lock).
|
||||||
|
# Whatever happens, it must not stay blank: put it back unless replaced.
|
||||||
Rdb::Fast.splice(table, r.from, r.to, Rdb::Fast.blanked(r.line) + "\n") unless o[:l]
|
Rdb::Fast.splice(table, r.from, r.to, Rdb::Fast.blanked(r.line) + "\n") unless o[:l]
|
||||||
new = edit_cells(data.head, cells.map(&:strip))
|
replaced = false
|
||||||
if new
|
begin
|
||||||
hit = Rdb::Fast::Hit.new(r, 0, 0)
|
new = edit_cells(data.head, cells.map(&:strip))
|
||||||
xf, xt = 0, 0
|
if new
|
||||||
if Rdb::Fast::SUFFIX[o[:m]]
|
hit = Rdb::Fast::Hit.new(r, 0, 0)
|
||||||
h = Rdb::Fast.search(Rdb::Fast::Data.new(table), o[:m], keycols, cells.values_at(*keycols.map { |c| data.column(c) }), first: true).first
|
xf, xt = 0, 0
|
||||||
xf, xt = h.xfrom, h.xto if h
|
if Rdb::Fast::SUFFIX[o[:m]]
|
||||||
|
h = Rdb::Fast.search(Rdb::Fast::Data.new(table), o[:m], keycols, cells.values_at(*keycols.map { |c| data.column(c) }), first: true).first
|
||||||
|
xf, xt = h.xfrom, h.xto if h
|
||||||
|
end
|
||||||
|
Rdb::Fast.replace_row(table, hit.row.from, hit.row.to, new, o[:m], xf, xt, keycols[0])
|
||||||
|
replaced = true
|
||||||
|
puts 'record replaced'
|
||||||
|
else
|
||||||
|
puts 'unchanged'
|
||||||
end
|
end
|
||||||
Rdb::Fast.replace_row(table, hit.row.from, hit.row.to, new, o[:m], xf, xt, keycols[0])
|
ensure
|
||||||
puts 'record replaced'
|
Rdb::Fast.splice(table, r.from, r.to, r.line + "\n") unless o[:l] || replaced
|
||||||
else
|
|
||||||
Rdb::Fast.splice(table, r.from, r.to, r.line + "\n")
|
|
||||||
puts 'unchanged'
|
|
||||||
end
|
end
|
||||||
next
|
next
|
||||||
when /\Ad/i
|
when /\Ad/i
|
||||||
@@ -393,7 +400,9 @@ Rdb.register('ve', 'Visually edit a table', 've [table [-n[n] -fc -mc -i -s [fil
|
|||||||
next puts('no such field') unless (0...t.head.size).cover?(f)
|
next puts('no such field') unless (0...t.head.size).cover?(f)
|
||||||
|
|
||||||
print "#{t.head[f]}> "
|
print "#{t.head[f]}> "
|
||||||
rows[cur][f] = ($stdin.gets || '').chomp
|
value = ($stdin.gets || '').chomp
|
||||||
|
Rdb.check_cells!([value])
|
||||||
|
rows[cur][f] = value
|
||||||
File.write(file, t.to_s)
|
File.write(file, t.to_s)
|
||||||
when 'a'
|
when 'a'
|
||||||
row = t.head.each_with_index.map do |h, i|
|
row = t.head.each_with_index.map do |h, i|
|
||||||
@@ -404,6 +413,7 @@ Rdb.register('ve', 'Visually edit a table', 've [table [-n[n] -fc -mc -i -s [fil
|
|||||||
print "#{h}> "
|
print "#{h}> "
|
||||||
($stdin.gets || '').chomp
|
($stdin.gets || '').chomp
|
||||||
end
|
end
|
||||||
|
Rdb.check_cells!(row)
|
||||||
rows << row
|
rows << row
|
||||||
File.write(file, t.to_s)
|
File.write(file, t.to_s)
|
||||||
cur = rows.size - 1
|
cur = rows.size - 1
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ Rdb.register('sorttable', 'Sort a table by one or more columns',
|
|||||||
end
|
end
|
||||||
cols = (0...t.head.size).to_a if cols.empty?
|
cols = (0...t.head.size).to_a if cols.empty?
|
||||||
key = ->(r) { cols.map { |i| numeric ? r[i].to_f : (fold ? r[i].downcase : r[i]) } }
|
key = ->(r) { cols.map { |i| numeric ? r[i].to_f : (fold ? r[i].downcase : r[i]) } }
|
||||||
sorted = t.rows.each_with_index.sort_by { |r, n| [key.(r), n] }.map(&:first)
|
keyed = t.rows.each_with_index.map { |r, n| [key.(r), n, r] }
|
||||||
sorted.reverse! if reverse
|
sorted = keyed.sort { |a, b| (reverse ? b[0] <=> a[0] : a[0] <=> b[0]).nonzero? || a[1] <=> b[1] }.map(&:last)
|
||||||
t.with(rows: sorted).write
|
t.with(rows: sorted).write
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -142,7 +142,7 @@ end
|
|||||||
Rdb.register('substitute', 'Replace old string with new string', 'substitute oldstring newstring file ...') do |args|
|
Rdb.register('substitute', 'Replace old string with new string', 'substitute oldstring newstring file ...') do |args|
|
||||||
die 'usage: substitute oldstring newstring file ...' if args.size < 3
|
die 'usage: substitute oldstring newstring file ...' if args.size < 3
|
||||||
old, new, *files = args
|
old, new, *files = args
|
||||||
files.each { |f| File.write(f, File.read(f).gsub(old, new)) }
|
files.each { |f| Rdb.write_file(f, File.read(f).gsub(old, new)) }
|
||||||
end
|
end
|
||||||
|
|
||||||
Rdb.register('trimblank', 'Remove leading and trailing blanks from a string', 'trimblank [string] [< file]') do |args|
|
Rdb.register('trimblank', 'Remove leading and trailing blanks from a string', 'trimblank [string] [< file]') do |args|
|
||||||
|
|||||||
+4
-4
@@ -50,7 +50,7 @@ module Rdb
|
|||||||
def icase_column(names, expr, i)
|
def icase_column(names, expr, i)
|
||||||
return unless Rdb.icase?
|
return unless Rdb.icase?
|
||||||
|
|
||||||
word = expr[i..][/\A\w+/].to_s
|
word = expr[i..][/\A[[:alnum:]_]+/].to_s
|
||||||
return if word.empty? || AWK_WORDS.include?(word) || expr[i + word.size..].to_s.match?(/\A\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) }
|
hits = names.select { |n, _| n.casecmp?(word) }
|
||||||
@@ -75,14 +75,14 @@ module Rdb
|
|||||||
j += expr[j] == '\\' ? 2 : 1 while j < expr.size && expr[j] != '/'
|
j += expr[j] == '\\' ? 2 : 1 while j < expr.size && expr[j] != '/'
|
||||||
out << expr[i..j]
|
out << expr[i..j]
|
||||||
i = j + 1
|
i = j + 1
|
||||||
elsif c.match?(/[A-Za-z_#]/) && (i.zero? || !expr[i - 1].match?(/[\w$]/))
|
elsif c.match?(/[[:alpha:]_#]/) && (i.zero? || !expr[i - 1].match?(/[[:alnum:]_$]/))
|
||||||
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?(/[[:alnum:]_]/) }
|
||||||
hit ||= icase_column(names, expr, i)
|
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
|
||||||
else
|
else
|
||||||
m = expr[i..][/\A\w+/] || c
|
m = expr[i..][/\A[[:alnum:]_]+/] || c
|
||||||
out << m
|
out << m
|
||||||
i += m.size
|
i += m.size
|
||||||
end
|
end
|
||||||
|
|||||||
+17
-15
@@ -42,7 +42,7 @@ module Rdb
|
|||||||
off += l.bytesize
|
off += l.bytesize
|
||||||
end
|
end
|
||||||
Rdb.die("#{path}: is a list; fast access needs a table") if bytes.start_with?("\n")
|
Rdb.die("#{path}: is a list; fast access needs a table") if bytes.start_with?("\n")
|
||||||
@head = lines.shift&.line.to_s.split("\t", -1).map(&:strip)
|
@head = lines.shift&.line.to_s.split("\t", -1).map { |h| h.dup.force_encoding(Encoding::UTF_8).strip }
|
||||||
lines.shift if lines.first && Table.dash_line?(lines.first.cells)
|
lines.shift if lines.first && Table.dash_line?(lines.first.cells)
|
||||||
@rows = lines
|
@rows = lines
|
||||||
@by_from = lines.to_h { |r| [r.from, r] }
|
@by_from = lines.to_h { |r| [r.from, r] }
|
||||||
@@ -95,22 +95,22 @@ module Rdb
|
|||||||
rows = live_rows(data)
|
rows = live_rows(data)
|
||||||
case method
|
case method
|
||||||
when 'r'
|
when 'r'
|
||||||
File.binwrite(path + '.r', FIXED_HEAD + rows.map { |r| fmt_entry(r.from) }.join)
|
Rdb.write_file(path + '.r', FIXED_HEAD + rows.map { |r| fmt_entry(r.from) }.join)
|
||||||
when 'i'
|
when 'i'
|
||||||
k = data.column(keycol)
|
k = data.column(keycol)
|
||||||
ents = rows.map { |r| [r.cells[k].to_s, r.from] }
|
ents = rows.map { |r| [r.cells[k].to_s, r.from] }
|
||||||
ents = ents.sort_by { |key, off| [fold ? key.downcase : key, off] }
|
ents = ents.sort_by { |key, off| [fold ? key.strip.downcase : key.strip, off] }
|
||||||
File.binwrite(path + '.i', "#{keycol}\tOffset\n#{'-' * keycol.size}\t------\n" + ents.map { |key, off| "#{key}\t#{off}\n" }.join)
|
Rdb.write_file(path + '.i', "#{keycol}\tOffset\n#{'-' * keycol.size}\t------\n" + ents.map { |key, off| "#{key}\t#{off}\n" }.join)
|
||||||
when 'h'
|
when 'h'
|
||||||
k = data.column(keycol)
|
k = data.column(keycol)
|
||||||
slots = [rows.size * 2, 2].max
|
slots = [rows.size * 2, 2].max
|
||||||
table = Array.new(slots, 0)
|
table = Array.new(slots, 0)
|
||||||
rows.each { |r| table[free_slot(table, hash_slot(slots, r.cells[k].to_s))] = r.from }
|
rows.each { |r| table[free_slot(table, hash_slot(slots, r.cells[k].to_s))] = r.from }
|
||||||
File.binwrite(path + '.h', FIXED_HEAD + table.map { |o| fmt_entry(o) }.join)
|
Rdb.write_file(path + '.h', FIXED_HEAD + table.map { |o| fmt_entry(o) }.join)
|
||||||
when 'b'
|
when 'b'
|
||||||
k = data.column(keycol)
|
k = data.column(keycol)
|
||||||
body = live_rows(data).sort_by { |r| r.cells[k].to_s }.map { |r| r.line + "\n" }
|
body = live_rows(data).sort_by { |r| r.cells[k].to_s.strip }.map { |r| r.line + "\n" }
|
||||||
File.binwrite(path, data.head.join("\t") + "\n" + Table.dash_row(data.head) + "\n" + body.join)
|
Rdb.write_file(path, data.head.join("\t") + "\n" + Table.dash_row(data.head) + "\n" + body.join)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ module Rdb
|
|||||||
|
|
||||||
def splice(path, from, to, text)
|
def splice(path, from, to, text)
|
||||||
b = File.binread(path)
|
b = File.binread(path)
|
||||||
File.binwrite(path, b.byteslice(0, from) + text + b.byteslice(to..).to_s)
|
Rdb.write_file(path, b.byteslice(0, from) + text + b.byteslice(to..).to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Remove a record's entry from the secondary index for method.
|
# Remove a record's entry from the secondary index for method.
|
||||||
@@ -227,11 +227,11 @@ module Rdb
|
|||||||
ip = path + '.r'
|
ip = path + '.r'
|
||||||
ents = read_fixed(ip)
|
ents = read_fixed(ip)
|
||||||
ents.delete(row_from)
|
ents.delete(row_from)
|
||||||
File.binwrite(ip, FIXED_HEAD + ents.map { |o| fmt_entry(o) }.join)
|
Rdb.write_file(ip, FIXED_HEAD + ents.map { |o| fmt_entry(o) }.join)
|
||||||
when 'h'
|
when 'h'
|
||||||
ip = path + '.h'
|
ip = path + '.h'
|
||||||
ents = read_fixed(ip).map { |o| o == row_from ? -1 : o }
|
ents = read_fixed(ip).map { |o| o == row_from ? -1 : o }
|
||||||
File.binwrite(ip, FIXED_HEAD + ents.map { |o| fmt_entry(o) }.join)
|
Rdb.write_file(ip, FIXED_HEAD + ents.map { |o| fmt_entry(o) }.join)
|
||||||
when 'i'
|
when 'i'
|
||||||
ip = path + '.i'
|
ip = path + '.i'
|
||||||
e = read_inverted(ip).find { |x| x[:offset] == row_from }
|
e = read_inverted(ip).find { |x| x[:offset] == row_from }
|
||||||
@@ -242,16 +242,16 @@ module Rdb
|
|||||||
def index_add(path, method, row_from, cells, data_head, keycol)
|
def index_add(path, method, row_from, cells, data_head, keycol)
|
||||||
case method
|
case method
|
||||||
when 'r'
|
when 'r'
|
||||||
File.binwrite(path + '.r', File.binread(path + '.r') + fmt_entry(row_from))
|
Rdb.write_file(path + '.r', File.binread(path + '.r') + fmt_entry(row_from))
|
||||||
when 'i'
|
when 'i'
|
||||||
ip = path + '.i'
|
ip = path + '.i'
|
||||||
name = File.binread(ip).lines.first.split("\t")[0]
|
name = File.binread(ip).lines.first.split("\t")[0]
|
||||||
key = cells[data_head.index(name)].to_s
|
key = cells[data_head.index(name)].to_s
|
||||||
ents = read_inverted(ip)
|
ents = read_inverted(ip)
|
||||||
pos = ents.find { |e| e[:key] > key }
|
pos = ents.find { |e| e[:key].strip > key.strip }
|
||||||
line = "#{key}\t#{row_from}\n"
|
line = "#{key}\t#{row_from}\n"
|
||||||
if pos then splice(ip, pos[:from], pos[:from], line)
|
if pos then splice(ip, pos[:from], pos[:from], line)
|
||||||
else File.binwrite(ip, File.binread(ip) + line)
|
else Rdb.write_file(ip, File.binread(ip) + line)
|
||||||
end
|
end
|
||||||
when 'h'
|
when 'h'
|
||||||
ip = path + '.h'
|
ip = path + '.h'
|
||||||
@@ -259,7 +259,7 @@ module Rdb
|
|||||||
if ents.any? { |o| o <= 0 }
|
if ents.any? { |o| o <= 0 }
|
||||||
key = cells[data_head.index(keycol)].to_s
|
key = cells[data_head.index(keycol)].to_s
|
||||||
ents[free_slot(ents, hash_slot(ents.size, key))] = row_from
|
ents[free_slot(ents, hash_slot(ents.size, key))] = row_from
|
||||||
File.binwrite(ip, FIXED_HEAD + ents.map { |o| fmt_entry(o) }.join)
|
Rdb.write_file(ip, FIXED_HEAD + ents.map { |o| fmt_entry(o) }.join)
|
||||||
else
|
else
|
||||||
build(path, 'h', keycol)
|
build(path, 'h', keycol)
|
||||||
end
|
end
|
||||||
@@ -269,11 +269,12 @@ module Rdb
|
|||||||
end
|
end
|
||||||
|
|
||||||
def append_row(path, cells, method, keycol)
|
def append_row(path, cells, method, keycol)
|
||||||
|
Rdb.check_cells!(cells)
|
||||||
line = cells.join("\t")
|
line = cells.join("\t")
|
||||||
b = File.binread(path)
|
b = File.binread(path)
|
||||||
b += "\n" unless b.empty? || b.end_with?("\n")
|
b += "\n" unless b.empty? || b.end_with?("\n")
|
||||||
from = b.bytesize
|
from = b.bytesize
|
||||||
File.binwrite(path, b + line + "\n")
|
Rdb.write_file(path, b + line + "\n")
|
||||||
index_add(path, method, from, cells, Data.new(path).head, keycol) if SUFFIX[method] || method == 'b'
|
index_add(path, method, from, cells, Data.new(path).head, keycol) if SUFFIX[method] || method == 'b'
|
||||||
from
|
from
|
||||||
end
|
end
|
||||||
@@ -287,6 +288,7 @@ module Rdb
|
|||||||
# Put cells where the record at hit was. If the new record is too long
|
# Put cells where the record at hit was. If the new record is too long
|
||||||
# even after trimming blanks, append it (unless append is false).
|
# even after trimming blanks, append it (unless append is false).
|
||||||
def replace_row(path, from, to, cells, method, xfrom, xto, keycol, allow_append: true)
|
def replace_row(path, from, to, cells, method, xfrom, xto, keycol, allow_append: true)
|
||||||
|
Rdb.check_cells!(cells)
|
||||||
room = to - from - 1
|
room = to - from - 1
|
||||||
line = cells.join("\t")
|
line = cells.join("\t")
|
||||||
line = cells.map(&:strip).join("\t") if line.bytesize > room
|
line = cells.map(&:strip).join("\t") if line.bytesize > room
|
||||||
|
|||||||
@@ -18,7 +18,18 @@ module Rdb
|
|||||||
cells.all? { |c| c.match?(/\A\s*-*\s*\z/) } && cells.any? { |c| c.include?('-') }
|
cells.all? { |c| c.match?(/\A\s*-*\s*\z/) } && cells.any? { |c| c.include?('-') }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# COHERENT-era files are often Latin-1. Text that is not valid UTF-8 is
|
||||||
|
# read as Latin-1 and comes out as UTF-8.
|
||||||
|
def self.utf8(text)
|
||||||
|
return text if text.encoding == Encoding::UTF_8 && text.valid_encoding?
|
||||||
|
|
||||||
|
text.dup.force_encoding(Encoding::UTF_8).then do |t|
|
||||||
|
t.valid_encoding? ? t : text.dup.force_encoding(Encoding::ISO_8859_1).encode(Encoding::UTF_8)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.parse(text)
|
def self.parse(text)
|
||||||
|
text = utf8(text)
|
||||||
return parse_list(text) if text.start_with?("\n")
|
return parse_list(text) if text.start_with?("\n")
|
||||||
|
|
||||||
lines = text.lines.map { |l| l.chomp("\n") }
|
lines = text.lines.map { |l| l.chomp("\n") }
|
||||||
|
|||||||
@@ -876,3 +876,51 @@ class TaxTablesTest < RdbCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class RobustnessTest < RdbCase
|
||||||
|
def test_umlauts_in_column_names
|
||||||
|
write('uml', "Größe\tÜber\n-----\t----\n5\t7\n1\t2\n")
|
||||||
|
assert_equal ['5', '7'], data(rdb('row', 'Über > 3', file: 'uml')).first
|
||||||
|
assert_equal 1, data(rdb('row', 'Größe > 3', file: 'uml')).size
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_latin1_data_is_read_and_written_as_utf8
|
||||||
|
File.binwrite(File.join(@dir, 'lat'), "Name\n----\n\xE4pfel\n")
|
||||||
|
assert_equal "Äpfel".downcase, data(rdb('row', 'Name ~ /pfel/', file: 'lat')).first.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_errors_are_one_line_not_a_backtrace
|
||||||
|
_o, e, st = run3('update.rdb', 'inventory', stdin: "/[\n")
|
||||||
|
refute st.success?
|
||||||
|
assert_match(/update\.rdb: .*char-class/, e)
|
||||||
|
refute_match(/\.rb:\d+:in/, e)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_enter_refuses_a_value_with_a_tab
|
||||||
|
_o, e, st = run3('enter', '-1', 'inventory', stdin: "9\t9\n1\n2\n3\nthing\n")
|
||||||
|
refute st.success?
|
||||||
|
assert_match(/contains a tab/, e)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_sorttable_reverse_keeps_ties_in_order
|
||||||
|
out = rdb('sorttable', '-r', 'B', stdin: "A\tB\n-\t-\n1\tb\n2\ta\n3\tb\n4\ta\n")
|
||||||
|
assert_equal %w[1 3 2 4], data(out).map(&:first)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_update_keeps_the_record_when_the_editor_fails
|
||||||
|
before = File.read(File.join(@dir, 'inventory'))
|
||||||
|
_o, e, = run3('update.rdb', 'inventory', stdin: "1\ne\nq\n", env: { 'EDITOR' => 'false' })
|
||||||
|
assert_match(/editor false failed/, e)
|
||||||
|
assert_equal before, File.read(File.join(@dir, 'inventory'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_write_file_is_complete_and_keeps_the_mode
|
||||||
|
path = File.join(@dir, 'wf')
|
||||||
|
File.write(path, 'old')
|
||||||
|
File.chmod(0o640, path)
|
||||||
|
Rdb.write_file(path, 'new')
|
||||||
|
assert_equal 'new', File.read(path)
|
||||||
|
assert_equal 0o640, File.stat(path).mode & 0o777
|
||||||
|
assert_empty Dir[File.join(@dir, 'wf.tmp*')]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user