Files
rdb/lib/rdb.rb
T
wkh 75c2424307 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.
2026-10-30 09:48:42 +01:00

88 lines
2.7 KiB
Ruby

# frozen_string_literal: true
# A Ruby re-creation of /rdb, the relational database tools Mark Williams
# Company sold for COHERENT (~1991).
require 'open3'
require 'date'
require 'tmpdir'
require 'fileutils'
require_relative 'rdb/version'
module Rdb
ROOT = File.expand_path('..', __dir__)
NUM = /\A\s*[+-]?(\d+\.?\d*|\.\d+)\s*\z/
Command = Struct.new(:name, :desc, :syntax, :block)
COMMANDS = {}
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)
abort "#{File.basename($PROGRAM_NAME)}: #{msg}"
end
# register 'name', 'description', 'syntax' do |args| ... end
def register(name, desc, syntax = nil, &block)
COMMANDS[name] = Command.new(name, desc, syntax || name, block)
end
def alias_for(name, target)
cmd = COMMANDS.fetch(target)
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
require_relative 'rdb/table'
require_relative 'rdb/env'
Dir[File.join(__dir__, 'rdb/cmd/*.rb')].sort.each { |f| require f }