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:
+29
@@ -46,9 +46,38 @@ module Rdb
|
||||
COMMANDS[name] = Command.new(name, cmd.desc, cmd.syntax.sub(target, name), cmd.block)
|
||||
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)
|
||||
cmd = COMMANDS[name] or die("unknown command '#{name}'")
|
||||
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
|
||||
|
||||
+25
-15
@@ -259,7 +259,7 @@ Rdb.register('vilock', 'Lock a table before editing, unlock afterward', 'vilock
|
||||
end
|
||||
File.unlink(file)
|
||||
begin
|
||||
system(*Shellwords.split(ENV['EDITOR'] || 'vi'), locked)
|
||||
system(*Shellwords.split(ENV['EDITOR'] || 'vi'), locked) or warn "vilock: editor #{ENV['EDITOR'] || 'vi'} failed"
|
||||
ensure
|
||||
File.rename(locked, file)
|
||||
end
|
||||
@@ -276,7 +276,7 @@ def edit_cells(head, cells)
|
||||
Tempfile.create('rdb-edit') do |f|
|
||||
f.write Rdb::Table.new(head, [cells], list: true).to_s
|
||||
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))
|
||||
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]
|
||||
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]
|
||||
new = edit_cells(data.head, cells.map(&:strip))
|
||||
if new
|
||||
hit = Rdb::Fast::Hit.new(r, 0, 0)
|
||||
xf, xt = 0, 0
|
||||
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
|
||||
replaced = false
|
||||
begin
|
||||
new = edit_cells(data.head, cells.map(&:strip))
|
||||
if new
|
||||
hit = Rdb::Fast::Hit.new(r, 0, 0)
|
||||
xf, xt = 0, 0
|
||||
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
|
||||
Rdb::Fast.replace_row(table, hit.row.from, hit.row.to, new, o[:m], xf, xt, keycols[0])
|
||||
puts 'record replaced'
|
||||
else
|
||||
Rdb::Fast.splice(table, r.from, r.to, r.line + "\n")
|
||||
puts 'unchanged'
|
||||
ensure
|
||||
Rdb::Fast.splice(table, r.from, r.to, r.line + "\n") unless o[:l] || replaced
|
||||
end
|
||||
next
|
||||
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)
|
||||
|
||||
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)
|
||||
when 'a'
|
||||
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}> "
|
||||
($stdin.gets || '').chomp
|
||||
end
|
||||
Rdb.check_cells!(row)
|
||||
rows << row
|
||||
File.write(file, t.to_s)
|
||||
cur = rows.size - 1
|
||||
|
||||
@@ -48,8 +48,8 @@ Rdb.register('sorttable', 'Sort a table by one or more columns',
|
||||
end
|
||||
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]) } }
|
||||
sorted = t.rows.each_with_index.sort_by { |r, n| [key.(r), n] }.map(&:first)
|
||||
sorted.reverse! if reverse
|
||||
keyed = t.rows.each_with_index.map { |r, n| [key.(r), n, r] }
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
+1
-1
@@ -142,7 +142,7 @@ end
|
||||
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
|
||||
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
|
||||
|
||||
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)
|
||||
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*\(/)
|
||||
|
||||
hits = names.select { |n, _| n.casecmp?(word) }
|
||||
@@ -75,14 +75,14 @@ module Rdb
|
||||
j += expr[j] == '\\' ? 2 : 1 while j < expr.size && expr[j] != '/'
|
||||
out << expr[i..j]
|
||||
i = j + 1
|
||||
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/) }
|
||||
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?(/[[:alnum:]_]/) }
|
||||
hit ||= icase_column(names, expr, i)
|
||||
if hit
|
||||
out << "$#{hit[1] + 1}"
|
||||
i += hit[0].size
|
||||
else
|
||||
m = expr[i..][/\A\w+/] || c
|
||||
m = expr[i..][/\A[[:alnum:]_]+/] || c
|
||||
out << m
|
||||
i += m.size
|
||||
end
|
||||
|
||||
+17
-15
@@ -42,7 +42,7 @@ module Rdb
|
||||
off += l.bytesize
|
||||
end
|
||||
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)
|
||||
@rows = lines
|
||||
@by_from = lines.to_h { |r| [r.from, r] }
|
||||
@@ -95,22 +95,22 @@ module Rdb
|
||||
rows = live_rows(data)
|
||||
case method
|
||||
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'
|
||||
k = data.column(keycol)
|
||||
ents = rows.map { |r| [r.cells[k].to_s, r.from] }
|
||||
ents = ents.sort_by { |key, off| [fold ? key.downcase : key, off] }
|
||||
File.binwrite(path + '.i', "#{keycol}\tOffset\n#{'-' * keycol.size}\t------\n" + ents.map { |key, off| "#{key}\t#{off}\n" }.join)
|
||||
ents = ents.sort_by { |key, off| [fold ? key.strip.downcase : key.strip, off] }
|
||||
Rdb.write_file(path + '.i', "#{keycol}\tOffset\n#{'-' * keycol.size}\t------\n" + ents.map { |key, off| "#{key}\t#{off}\n" }.join)
|
||||
when 'h'
|
||||
k = data.column(keycol)
|
||||
slots = [rows.size * 2, 2].max
|
||||
table = Array.new(slots, 0)
|
||||
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'
|
||||
k = data.column(keycol)
|
||||
body = live_rows(data).sort_by { |r| r.cells[k].to_s }.map { |r| r.line + "\n" }
|
||||
File.binwrite(path, data.head.join("\t") + "\n" + Table.dash_row(data.head) + "\n" + body.join)
|
||||
body = live_rows(data).sort_by { |r| r.cells[k].to_s.strip }.map { |r| r.line + "\n" }
|
||||
Rdb.write_file(path, data.head.join("\t") + "\n" + Table.dash_row(data.head) + "\n" + body.join)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -217,7 +217,7 @@ module Rdb
|
||||
|
||||
def splice(path, from, to, text)
|
||||
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
|
||||
|
||||
# Remove a record's entry from the secondary index for method.
|
||||
@@ -227,11 +227,11 @@ module Rdb
|
||||
ip = path + '.r'
|
||||
ents = read_fixed(ip)
|
||||
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'
|
||||
ip = path + '.h'
|
||||
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'
|
||||
ip = path + '.i'
|
||||
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)
|
||||
case method
|
||||
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'
|
||||
ip = path + '.i'
|
||||
name = File.binread(ip).lines.first.split("\t")[0]
|
||||
key = cells[data_head.index(name)].to_s
|
||||
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"
|
||||
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
|
||||
when 'h'
|
||||
ip = path + '.h'
|
||||
@@ -259,7 +259,7 @@ module Rdb
|
||||
if ents.any? { |o| o <= 0 }
|
||||
key = cells[data_head.index(keycol)].to_s
|
||||
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
|
||||
build(path, 'h', keycol)
|
||||
end
|
||||
@@ -269,11 +269,12 @@ module Rdb
|
||||
end
|
||||
|
||||
def append_row(path, cells, method, keycol)
|
||||
Rdb.check_cells!(cells)
|
||||
line = cells.join("\t")
|
||||
b = File.binread(path)
|
||||
b += "\n" unless b.empty? || b.end_with?("\n")
|
||||
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'
|
||||
from
|
||||
end
|
||||
@@ -287,6 +288,7 @@ module Rdb
|
||||
# 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).
|
||||
def replace_row(path, from, to, cells, method, xfrom, xto, keycol, allow_append: true)
|
||||
Rdb.check_cells!(cells)
|
||||
room = to - from - 1
|
||||
line = cells.join("\t")
|
||||
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?('-') }
|
||||
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)
|
||||
text = utf8(text)
|
||||
return parse_list(text) if text.start_with?("\n")
|
||||
|
||||
lines = text.lines.map { |l| l.chomp("\n") }
|
||||
|
||||
Reference in New Issue
Block a user