commit c85cd9b72c57aedcaec989c8910a72ac3df46d32
parent 94ad0043622327173f2d441116da2304ce4ccb48
Author: ssnf <ssnf@ssnf.xyz>
Date: Sun, 3 Aug 2025 08:30:59 +0000
sdb: fix bugs and add documentation
Diffstat:
11 files changed, 451 insertions(+), 32 deletions(-)
diff --git a/man/man1/INDEX b/man/man1/INDEX
@@ -194,6 +194,13 @@ sam.save sam.1
samsave sam.1
samterm sam.1
scat scat.1
+sdbedit sdb.1
+sdbjoin sdb.1
+sdbmap sdb.1
+sdbpretty sdb.1
+sdbquery sdb.1
+sdbrval sdb.1
+sdbuniq sdb.1
aescbc secstore.1
ipso secstore.1
secstore secstore.1
diff --git a/man/man1/sdb.1 b/man/man1/sdb.1
@@ -0,0 +1,301 @@
+.TH SDB 1
+.SH NAME
+sdbquery, sdbedit, sdbjoin, sdbmap, sdbpretty, sdbrval, sdbuniq \- simple database
+.SH SYNOPSIS
+.B sdbquery
+[
+.B -f
+.I file
+]
+.I "attr value"
+[
+.I "attr value"
+]
+\&...
+.br
+.B sdbedit
+[
+.B -f
+.I file
+]
+.I "attr value"
+[
+.I "attr value"
+]
+\&...
+.br
+.B sdbjoin
+[
+.B -f
+.I file0
+]
+.I file1
+[
+.I "attr value"
+]
+\&...
+.br
+.B sdbmap
+[
+.B -k
+.I attr
+]
+[
+.B -f
+.I file0
+]
+.I mapfile
+.I source-attr
+.I dest-attr
+.br
+.B sdbpretty
+[
+.B -f
+.I file
+]
+.br
+.B sdbrval
+[
+.B -am
+]
+.I attr
+\&...
+.br
+.B sdbuniq
+[
+.B -f
+.I file
+]
+[
+.I attr
+]
+\&...
+.SH DESCRIPTION
+These commands operate on simple databases using the same format as Plan 9's
+network database (ndb).
+Database files contain records with attribute=value pairs,
+with records separated by blank lines and
+continuation lines starting with whitespace.
+Comments begin with '#'.
+.PP
+.I Sdbquery
+searches the database for records matching the given attribute-value pairs.
+With
+.BR -f ,
+it reads from the specified
+.IR file ;
+otherwise it reads from standard input.
+All records matching the query are printed.
+.PP
+.I Sdbedit
+finds records matching the first attribute-value pair (the query)
+and adds or updates them with the remaining attribute-value pairs (the edits).
+If no matching record is found, it creates a new record.
+With
+.BR -f ,
+it reads from the specified
+.IR file ;
+otherwise it reads from standard input.
+The updated database is written to standard output.
+.PP
+.I Sdbjoin
+performs a database join operation between two files.
+For each record in
+.I file0
+(or standard input if
+.B -f
+is not given),
+it uses the record's first attribute-value pair as a query to find matching records in
+.IR file1 .
+Records from
+.I file1
+that contain the queried attribute-value pair are joined with the
+.I file0
+record.
+Additional attribute-value constraints can be specified.
+The joined records are written to standard output.
+.PP
+.I Sdbmap
+uses a mapping database
+.I mapfile
+to transform values in the input database.
+It looks up values of
+.I source-attr
+in the mapping database and replaces them with the corresponding
+.I dest-attr
+values.
+With
+.BR -k ,
+it uses
+.I attr
+as the key attribute in the mapping database;
+otherwise it uses
+.IR source-attr .
+With
+.BR -f ,
+it reads from the specified
+.IR file0 ;
+otherwise it reads from standard input.
+.PP
+.I Sdbpretty
+formats database records for display, printing each record with
+the first attribute on its own line and subsequent attributes indented.
+With
+.BR -f ,
+it reads from the specified
+.IR file ;
+otherwise it reads from standard input.
+.PP
+.I Sdbrval
+extracts specified attribute values from database records.
+With
+.BR -a ,
+it prints attribute names and values in the form
+.BR attr=value .
+With
+.BR -m ,
+it uses pattern matching to select attributes.
+It reads from standard input and prints tab-separated values for each record.
+.PP
+.I Sdbuniq
+removes duplicate records based on specified key attributes.
+Input must be sorted by the first attribute and key attributes for correct operation.
+Records are considered duplicates if they have the same value for the first attribute
+and the same values for all specified key attributes.
+When duplicates are found, it merges their attributes and prints the combined record.
+Only records with more than one attribute beyond the specified keys are output.
+With
+.BR -f ,
+it reads from the specified
+.IR file ;
+otherwise it reads from standard input.
+.SH EXAMPLES
+Create a starship database:
+.IP
+.EX
+% cat >ships.db
+ship=enterprise class=constitution captain=kirk
+ crew=430 warp=8 phasers=12 torpedoes=6
+
+ship=defiant class=defiant captain=sisko
+ crew=50 warp=9.5 cloak=yes quantum=40
+
+ship=voyager class=intrepid captain=janeway
+ crew=150 warp=9.975 bioneural=yes delta=far
+.EE
+.PP
+Find warp-capable ships commanded by captains named 'k*':
+.IP
+.EX
+% sdbquery -f ships.db captain 'k*'
+ship=enterprise class=constitution captain=kirk
+ crew=430 warp=8 phasers=12 torpedoes=6
+.EE
+.PP
+Transfer command and update crew complement:
+.IP
+.EX
+% sdbquery -f ships.db ship voyager |
+ sdbedit ship voyager captain chakotay crew 147
+ship=voyager class=intrepid captain=chakotay
+ crew=147 warp=9.975 bioneural=yes delta=far
+.EE
+.PP
+Create quantum torpedo inventory and map to ships:
+.IP
+.EX
+% cat >weapons.db
+quantum=40 type=photon power=high
+ federation=yes klingon=no
+
+quantum=6 type=phaser power=medium
+ federation=yes cardassian=yes
+% sdbquery -f ships.db quantum '*' |
+ sdbmap weapons.db quantum type
+ship=defiant class=defiant captain=sisko
+ crew=50 warp=9.5 cloak=yes type=photon
+.EE
+.PP
+Analyze fleet capabilities by extracting warp factors:
+.IP
+.EX
+% sdbquery -f ships.db ship '*' | sdbrval -a warp | sort -t= -k2 -n
+warp=8
+warp=9.5
+warp=9.975
+.EE
+.PP
+Join ship records with their class specifications:
+.IP
+.EX
+% cat >classes.db
+ship=enterprise registry=NCC-1701 commissioned=2245
+ designer=cochrane yards=sf-fleet
+
+ship=defiant registry=NX-74205 commissioned=2366
+ designer=sisko yards=utopia-planitia
+
+ship=voyager registry=NCC-74656 commissioned=2371
+ designer=janeway yards=utopia-planitia
+% sdbjoin -f ships.db classes.db
+ship=enterprise class=constitution captain=kirk
+ crew=430 warp=8 phasers=12 torpedoes=6
+ registry=NCC-1701 commissioned=2245
+ designer=cochrane yards=sf-fleet
+ship=defiant class=defiant captain=sisko
+ crew=50 warp=9.5 cloak=yes quantum=40
+ registry=NX-74205 commissioned=2366
+ designer=sisko yards=utopia-planitia
+ship=voyager class=intrepid captain=janeway
+ crew=150 warp=9.975 bioneural=yes delta=far
+ registry=NCC-74656 commissioned=2371
+ designer=janeway yards=utopia-planitia
+.EE
+.PP
+Monitor temporal anomalies with duplicate detection:
+.IP
+.EX
+% cat >timeline.db
+ship=enterprise date=2267 incident=mirror_universe
+ spock=bearded kirk=evil
+
+ship=enterprise date=2267 incident=mirror_universe
+ mccoy=savage uhura=rebel
+
+ship=defiant date=2375 incident=mirror_universe
+ sisko=terran kira=intendant odo=security
+% sdbuniq -f timeline.db date incident
+ship=enterprise date=2267 incident=mirror_universe
+ spock=bearded kirk=evil mccoy=savage
+ uhura=rebel
+ship=defiant date=2375 incident=mirror_universe
+ sisko=terran kira=intendant odo=security
+.EE
+.PP
+Format fleet status for admiralty briefing:
+.IP
+.EX
+% sdbpretty <ships.db
+ship=enterprise
+ class=constitution
+ captain=kirk
+ crew=430
+ warp=8
+ phasers=12
+ torpedoes=6
+ship=defiant
+ class=defiant
+ captain=sisko
+ crew=50
+ warp=9.5
+ cloak=yes
+ quantum=40
+.EE
+.SH SOURCE
+.B \*9/src/cmd/sdb
+.SH SEE ALSO
+.MR ndb (1) ,
+.MR ndb (3) ,
+.MR ndb (7) ,
+.MR sdb (3) ,
+.MR sdbr (3)
+\ No newline at end of file
diff --git a/man/man3/sdbr.3 b/man/man3/sdbr.3
@@ -135,8 +135,8 @@ parses a string
containing space-separated attribute=value pairs and adds them to record
.IR r .
The input string is not modified.
-Values containing spaces can be quoted with double quotes.
-When values are quoted, the following characters are escaped: \e0, \ef, \en, \er, \et, \ev, \e", and \e\e.
+Value quoting and escaping are described in
+.MR sdb (7) .
.PP
.I Sdbr_arg2r
parses command-line arguments
@@ -165,13 +165,8 @@ tests whether
.I value
matches
.IR pattern .
-The pattern supports wildcards (*) and comparison operators
-(>, >=, <, <=).
-The * wildcard matches zero or more characters.
-When a * is followed by a literal character, it finds the first occurrence
-of that character in the remaining string and continues matching from there.
-Comparison operators perform lexicographic string comparison, not numeric.
-Literal asterisks can be escaped with backslash.
+Pattern matching is described in
+.MR sdb (7) .
.PP
.I Sdbr_query
tests whether record
diff --git a/man/man7/INDEX b/man/man7/INDEX
@@ -16,6 +16,7 @@ ndb ndb.7
plot plot.7
plumb plumb.7
regexp regexp.7
+sdb sdb.7
thumbprint thumbprint.7
ASCII utf.7
UTF utf.7
diff --git a/man/man7/sdb.7 b/man/man7/sdb.7
@@ -0,0 +1,99 @@
+.TH SDB 7
+.SH NAME
+sdb \- simple database format
+.SH DESCRIPTION
+The simple database (sdb) format is a text-based database format
+that is 100% backward compatible with Plan 9's network database (ndb) format.
+Database files contain records with attribute=value pairs,
+with records separated by blank lines.
+.PP
+Each record consists of one or more lines.
+The first line contains space-separated attribute=value pairs.
+Continuation lines begin with whitespace (spaces or tabs) and
+contain additional attribute=value pairs for the same record.
+.PP
+Within a line, attribute=value pairs are separated by spaces or tabs.
+The attribute name and value are separated by an equals sign (=).
+Attribute names cannot contain equals signs or whitespace.
+.PP
+Values can be quoted with double quotes to include spaces.
+When values are quoted, the following escape sequences are recognized:
+\e0 (null), \ef (form feed), \en (newline), \er (carriage return),
+\et (tab), \ev (vertical tab), \e" (quote), and \e\e (backslash).
+All other bytes, including those with values above 127, can be stored directly.
+SDB fully supports binary data.
+.PP
+When values are not quoted, tabs cannot appear in values as they
+are field separators. To include a tab in a value, quote the value
+and use \et.
+.PP
+Comments begin with # and extend to the end of the line.
+Comments can appear on their own line or after attribute=value pairs.
+.SH PATTERN MATCHING
+Several sdb tools support pattern matching for attribute values.
+The pattern language includes:
+.TP
+.B *
+Wildcard that matches zero or more characters.
+However, a pattern consisting of a single * matches one or more characters.
+.TP
+.B >
+.PD 0
+.TP
+.B >=
+.TP
+.B <
+.TP
+.B <=
+.PD
+Comparison operators for lexicographic string comparison.
+.TP
+.B \e*
+Literal asterisk (escaped with backslash).
+.PP
+When a * is followed by a literal character, it finds the first occurrence
+of that character in the remaining string and continues matching from there.
+.SH EXAMPLES
+A simple database of network services:
+.IP
+.EX
+service=http port=80 protocol=tcp
+ description="Web server"
+
+service=https port=443 protocol=tcp
+ description="Secure web server"
+
+service=ssh port=22 protocol=tcp
+ description="Secure shell" security=high
+.EE
+.PP
+Using special characters in values:
+.IP
+.EX
+# Tab-separated data
+data=raw value="one\ttwo\tthree"
+
+# Multi-line text
+message="First line\enSecond line\enThird line"
+
+# Path with spaces
+file="/home/user/My Documents/report.txt"
+
+# Binary data with null bytes and high-bit characters
+signature="GIF89a\0\0ÿÿ"
+checksum="Á¢£¤¥¦§¨©"
+.EE
+.SH SEE ALSO
+.MR ndb (1) ,
+.MR ndb (7) ,
+.MR sdb (1) ,
+.MR sdb (3) ,
+.MR sdbr (3)
+.SH NOTES
+The sdb format is designed to be both human-readable and
+suitable for processing with standard Unix text tools.
+Single-line record format enables easy sorting and filtering
+with tools like
+.MR sort (1)
+and
+.MR grep (1) .
+\ No newline at end of file
diff --git a/src/cmd/sdb/sdbedit.c b/src/cmd/sdb/sdbedit.c
@@ -5,7 +5,7 @@ static char *file;
static void
usage(void)
{
- fprint(2, "sdbedit: [-f file] attr0 value0 [attr1 value1]...");
+ fprint(2, "sdbedit: [-f file] attr0 value0 [attr1 value1]...\n");
exits("usage");
}
diff --git a/src/cmd/sdb/sdbrval.c b/src/cmd/sdb/sdbrval.c
@@ -3,6 +3,7 @@
static void print_col(Sdbr*, char*);
static String val;
+static String ln;
static char *fmt = "%.0s%s";
static void (*print_fn)(Sdbr*, char*) = print_col;
@@ -11,13 +12,15 @@ print_col(Sdbr *r, char *attr)
{
int n;
+ if (ln.n)
+ Straddc(&ln, '\t');
n = sdbr_attr(*r, attr);
if (n < 0) {
- print(fmt, attr, "");
+ Strprint(&ln, fmt, attr, "");
return;
}
sdbr_escape(&val, r->val[n]);
- print(fmt, r->attr[n].s, val.s);
+ Strprint(&ln, fmt, r->attr[n].s, val.s);
}
static void
@@ -28,8 +31,10 @@ print_match(Sdbr *r, char *attr)
for (i = 0; i < sdbr_n(*r); ++i) {
if (!sdbr_match(attr, r->attr[i].s))
continue;
+ if (ln.n)
+ Straddc(&ln, '\t');
sdbr_escape(&val, r->val[i]);
- print(fmt, r->attr[i].s, val.s);
+ Strprint(&ln, fmt, r->attr[i].s, val.s);
}
}
@@ -60,15 +65,15 @@ main(int argc, char *argv[])
if (argc < 1)
usage();
Strinit(&val);
+ Strinit(&ln);
sdb_open(&db, nil);
for (;sdb_next(&db);) {
r = db.r + db.n;
- print_fn(r, argv[0]);
- for (n = 1; n < argc; ++n) {
- write(1, "\t", 1);
+ Strzero(&ln);
+ for (n = 0; n < argc; ++n)
print_fn(r, argv[n]);
- }
- write(1, "\n", 1);
+ if (ln.n || print_fn == print_col)
+ print("%s\n", ln.s);
}
exits(0);
}
diff --git a/src/cmd/sdb/sdbuniq.c b/src/cmd/sdb/sdbuniq.c
@@ -56,7 +56,8 @@ main(int argc, char *argv[])
sdbr_init(&nr);
sdbr_init(&r);
sdb_open(&db, file);
- sdb_next(&db);
+ if (!sdb_next(&db))
+ exits(0);
sdbr_dup(&cr, db.r[db.n]);
sdbr_dup(&nr, db.r[db.n]);
for (;sdb_next(&db);) {
diff --git a/src/libsdb/sdb_next.c b/src/libsdb/sdb_next.c
@@ -9,29 +9,31 @@ sdb_next(Sdb *db)
if (db->n + 1 < Vecsiz(db->r))
return ++db->n;
if (!db->ln.n) {
- for (;Strgets(&db->ln, db->b);) {
+ for (;;) {
+ if (!Strgets(&db->ln, db->b))
+ return 0;
for (n = 0; n < db->ln.n
&& isspace(db->ln.s[n]); ++n);
- if (db->ln.s[n] != '#'
- && !isspace(db->ln.s[n]))
+ if (n < db->ln.n && db->ln.s[n] != '#')
break;
}
- if (!db->ln.n)
- return 0;
}
if (Vecsiz(db->r) && Vecsiz(db->r[db->n].attr))
++db->n;
Strinit(&s);
- Stradds(&s, db->ln);
- for (;Strgets(&db->ln, db->b);) {
- if (db->ln.s[0] != '#' && !isspace(db->ln.s[0]))
- break;
+ for (;;) {
for (n = 0; n < db->ln.n; ++n)
if (db->ln.s[n] == '#') {
Strdelete(&db->ln, n, db->ln.n);
break;
}
Stradds(&s, db->ln);
+ if (!Strgets(&db->ln, db->b)) {
+ Strzero(&db->ln);
+ break;
+ }
+ if (!isspace(db->ln.s[0]))
+ break;
}
sdbr_str2r(Vecadd(&db->r), s);
Strclose(&s);
diff --git a/src/libsdbr/sdbr_match.c b/src/libsdbr/sdbr_match.c
@@ -9,6 +9,8 @@ sdbr_match(char *k, char *v)
sysfatal("sdbr_match: nil key");
if (!v)
sysfatal("sdbr_match: nil value");
+ if (k[0] == '*' && k[1] == '\0')
+ return *v ? 1 : 0;
if (*k == '>') {
if (*++k == '=')
return strcmp(v, k + 1) >= 0 ? 1 : 0;
diff --git a/src/libsdbr/sdbr_r2str.c b/src/libsdbr/sdbr_r2str.c
@@ -10,10 +10,14 @@ sdbr_r2str(String *s, Sdbr r)
sysfatal("sdbr_r2str: empty record");
Strinit(&t);
Strzero(s);
- Strprint(s, "%s=%s", r.attr[0].s, sdbr_escape(&t, r.val[0]));
- for (i = 1; i < Vecsiz(r.attr); ++i)
- Strprint(s, "\t%s=%s", r.attr[i].s
- , sdbr_escape(&t, r.val[i]));
+ Strprint(s, "%s=", r.attr[0].s);
+ sdbr_escape(&t, r.val[0]);
+ Stradds(s, t);
+ for (i = 1; i < Vecsiz(r.attr); ++i) {
+ Strprint(s, "\t%s=", r.attr[i].s);
+ sdbr_escape(&t, r.val[i]);
+ Stradds(s, t);
+ }
Straddc(s, '\n');
Strclose(&t);
}