Skip to content

Commit d3044d8

Browse files
committed
Add command-line interface for dbm module
1 parent 7636a66 commit d3044d8

File tree

4 files changed

+255
-8
lines changed

4 files changed

+255
-8
lines changed

Doc/library/dbm.rst

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,3 +501,70 @@ The :mod:`!dbm.dumb` module defines the following:
501501
that this factor changes for each :mod:`dbm` submodule.
502502

503503
.. versionadded:: next
504+
505+
506+
.. _dbm-commandline:
507+
.. program:: dbm
508+
509+
Command-line interface
510+
----------------------
511+
512+
.. module:: dbm.__main__
513+
:synopsis: A command-line interface for DBM database operations.
514+
515+
**Source code:** :source:`Lib/dbm/__main__.py`
516+
517+
--------------
518+
519+
The :mod:`dbm` module can be invoked as a script via ``python -m dbm``
520+
to identify, examine, and reorganize DBM database files.
521+
522+
Command-line options
523+
^^^^^^^^^^^^^^^^^^^^
524+
525+
.. option:: --whichdb file [file ...]
526+
527+
Identify the database type for one or more database files:
528+
529+
.. code-block:: shell-session
530+
531+
$ python -m dbm --whichdb *.db
532+
dbm.gnu - database1.db
533+
dbm.sqlite3 - database2.db
534+
UNKNOWN - corrupted.db
535+
536+
This command uses the :func:`whichdb` function to determine the type
537+
of each database file. Files that cannot be identified are marked as
538+
``UNKNOWN``.
539+
540+
.. option:: --dump file
541+
542+
Display the contents of a database file:
543+
544+
.. code-block:: shell-session
545+
546+
$ python -m dbm --dump mydb.db
547+
username: john_doe
548+
549+
last_login: 2024-01-15
550+
551+
Keys and values are displayed in ``key: value`` format. Binary data
552+
is decoded using UTF-8 with error replacement for display purposes.
553+
554+
.. option:: --reorganize file
555+
556+
Reorganize and compact a database file to reduce disk space:
557+
558+
.. code-block:: shell-session
559+
560+
$ python -m dbm --reorganize mydb.db
561+
Reorganized database 'mydb.db'
562+
563+
This operation uses the database's native :meth:`!reorganize` method
564+
when available (:mod:`dbm.sqlite3`, :mod:`dbm.gnu`, :mod:`dbm.dumb`).
565+
For database types that don't support reorganization, an error message
566+
is displayed.
567+
568+
.. option:: -h, --help
569+
570+
Show the help message.

Lib/dbm/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import io
3333
import os
3434
import struct
35-
import sys
3635

3736

3837
class error(Exception):
@@ -187,8 +186,3 @@ def whichdb(filename):
187186

188187
# Unknown
189188
return ""
190-
191-
192-
if __name__ == "__main__":
193-
for filename in sys.argv[1:]:
194-
print(whichdb(filename) or "UNKNOWN", filename)

Lib/dbm/__main__.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import argparse
2+
import os
3+
import sys
4+
5+
from . import open as dbm_open, whichdb, error
6+
7+
8+
def _whichdb_command(filenames):
9+
exit_code = 0
10+
11+
for filename in filenames:
12+
if os.path.exists(filename):
13+
db_type = whichdb(filename)
14+
print(f"{db_type or 'UNKNOWN'} - {filename}")
15+
else:
16+
print(f"Error: File '{filename}' not found", file=sys.stderr)
17+
exit_code = 1
18+
19+
return exit_code
20+
21+
22+
def _dump_command(filename):
23+
try:
24+
with dbm_open(filename, "r") as db:
25+
for key in db.keys():
26+
key_str = key.decode("utf-8", errors="replace")
27+
value_str = db[key].decode("utf-8", errors="replace")
28+
print(f"{key_str}: {value_str}")
29+
return 0
30+
except error:
31+
print(f"Error: Database '{filename}' not found", file=sys.stderr)
32+
return 1
33+
34+
35+
def _reorganize_command(filename):
36+
try:
37+
with dbm_open(filename, "c") as db:
38+
if whichdb(filename) in ["dbm.sqlite3", "dbm.gnu", "dbm.dumb"]:
39+
db.reorganize()
40+
print(f"Reorganized database '{filename}'")
41+
else:
42+
print(
43+
f"Database type doesn't support reorganize method",
44+
file=sys.stderr,
45+
)
46+
return 1
47+
return 0
48+
except error:
49+
print(
50+
f"Error: Database '{filename}' not found or cannot be opened",
51+
file=sys.stderr,
52+
)
53+
return 1
54+
55+
56+
def main():
57+
parser = argparse.ArgumentParser(
58+
prog="python -m dbm", description="DBM toolkit"
59+
)
60+
group = parser.add_mutually_exclusive_group(required=True)
61+
group.add_argument(
62+
"--whichdb",
63+
nargs="+",
64+
metavar="file",
65+
help="Identify database type for one or more files",
66+
)
67+
group.add_argument(
68+
"--dump", metavar="file", help="Display database contents"
69+
)
70+
group.add_argument(
71+
"--reorganize",
72+
metavar="file",
73+
help="Reorganize the database",
74+
)
75+
options = parser.parse_args()
76+
77+
try:
78+
if options.whichdb:
79+
return _whichdb_command(options.whichdb)
80+
elif options.dump:
81+
return _dump_command(options.dump)
82+
elif options.reorganize:
83+
return _reorganize_command(options.reorganize)
84+
except KeyboardInterrupt:
85+
return 1
86+
87+
88+
if __name__ == "__main__":
89+
sys.exit(main())

Lib/test/test_dbm.py

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
"""Test script for the dbm.open function based on testdumbdbm.py"""
2-
31
import unittest
42
import dbm
53
import os
4+
import contextlib
5+
import io
6+
import sys
7+
68
from test.support import import_helper
79
from test.support import os_helper
10+
from test.support.script_helper import assert_python_ok, assert_python_failure
811

12+
from dbm.__main__ import main as dbm_main
913

1014
try:
1115
from dbm import sqlite3 as dbm_sqlite3
@@ -309,6 +313,99 @@ def setUp(self):
309313
self.dbm = import_helper.import_fresh_module('dbm')
310314

311315

316+
class DBMCommandLineTestCase(unittest.TestCase):
317+
318+
def setUp(self):
319+
self.addCleanup(cleaunup_test_dir)
320+
setup_test_dir()
321+
self.test_db = os.path.join(dirname, 'test.db')
322+
with dbm.open(self.test_db, 'c') as db:
323+
db[b'key1'] = b'value1'
324+
db[b'key2'] = b'value2'
325+
self.empty_db = os.path.join(dirname, 'empty.db')
326+
with dbm.open(self.empty_db, 'c'):
327+
pass
328+
self.dbm = import_helper.import_fresh_module('dbm')
329+
330+
def run_cmd_ok(self, *args):
331+
return assert_python_ok('-m', 'dbm', *args).out
332+
333+
def run_cmd_error(self, *args):
334+
return assert_python_failure('-m', 'dbm', *args)
335+
336+
def test_help(self):
337+
output = self.run_cmd_ok('-h')
338+
self.assertIn(b'usage:', output)
339+
self.assertIn(b'python -m dbm', output)
340+
self.assertIn(b'--help', output)
341+
self.assertIn(b'whichdb', output)
342+
self.assertIn(b'dump', output)
343+
self.assertIn(b'reorganize', output)
344+
345+
def test_whichdb_command(self):
346+
output = self.run_cmd_ok('--whichdb', self.test_db)
347+
self.assertIn(self.test_db.encode(), output)
348+
output = self.run_cmd_ok('--whichdb', self.test_db, self.empty_db)
349+
self.assertIn(self.test_db.encode(), output)
350+
self.assertIn(self.empty_db.encode(), output)
351+
352+
def test_whichdb_nonexistent_file(self):
353+
rc, _, stderr = self.run_cmd_error('--whichdb', "nonexistent_db")
354+
self.assertEqual(rc, 1)
355+
self.assertIn(b'not found', stderr)
356+
357+
def test_whichdb_unknown_format(self):
358+
text_file = os.path.join(dirname, 'text.txt')
359+
with open(text_file, 'w') as f:
360+
f.write('This is not a database file')
361+
output = self.run_cmd_ok('--whichdb', text_file)
362+
self.assertIn(b'UNKNOWN', output)
363+
self.assertIn(text_file.encode(), output)
364+
365+
def test_whichdb_output_format(self):
366+
output = self.run_cmd_ok('--whichdb', self.test_db)
367+
output_str = output.decode('utf-8', errors='replace').strip()
368+
# Should be "TYPE - FILENAME" format
369+
self.assertIn(' - ', output_str)
370+
parts = output_str.split(' - ', 1)
371+
self.assertEqual(len(parts), 2)
372+
self.assertEqual(parts[1], self.test_db)
373+
374+
def test_dump_command(self):
375+
output = self.run_cmd_ok('--dump', self.test_db)
376+
self.assertIn(b'key1: value1', output)
377+
self.assertIn(b'key2: value2', output)
378+
379+
def test_dump_empty_database(self):
380+
output = self.run_cmd_ok('--dump', self.empty_db)
381+
self.assertEqual(output.strip(), b'')
382+
383+
def test_dump_nonexistent_database(self):
384+
rc, _, stderr = self.run_cmd_error('--dump', "nonexistent_db")
385+
self.assertEqual(rc, 1)
386+
self.assertIn(b'not found', stderr)
387+
388+
def test_reorganize_command(self):
389+
self.addCleanup(setattr, dbm, '_defaultmod', dbm._defaultmod)
390+
for module in dbm_iterator():
391+
setup_test_dir()
392+
dbm._defaultmod = module
393+
with module.open(_fname, 'c') as f:
394+
f[b"1"] = b"1"
395+
if hasattr(module, 'reorganize'):
396+
with module.open(_fname, 'c') as db:
397+
output = self.run_cmd_ok('--reorganize', db)
398+
self.assertIn(b'Reorganized', output)
399+
400+
def test_output_format_consistency(self):
401+
output = self.run_cmd_ok('--dump', self.test_db)
402+
lines = output.decode('utf-8', errors='replace').strip().split('\n')
403+
for line in lines:
404+
if line.strip(): # Skip empty lines
405+
self.assertIn(':', line)
406+
parts = line.split(':', 1)
407+
self.assertEqual(len(parts), 2)
408+
312409
for mod in dbm_iterator():
313410
assert mod.__name__.startswith('dbm.')
314411
suffix = mod.__name__[4:]

0 commit comments

Comments
 (0)