diff --git a/Doc/library/dbm.rst b/Doc/library/dbm.rst index 39e287b15214e4..8c1a3f7754467f 100644 --- a/Doc/library/dbm.rst +++ b/Doc/library/dbm.rst @@ -501,3 +501,70 @@ The :mod:`!dbm.dumb` module defines the following: that this factor changes for each :mod:`dbm` submodule. .. versionadded:: next + + +.. _dbm-commandline: +.. program:: dbm + +Command-line interface +---------------------- + +.. module:: dbm.__main__ + :synopsis: A command-line interface for DBM database operations. + +**Source code:** :source:`Lib/dbm/__main__.py` + +-------------- + +The :mod:`dbm` module can be invoked as a script via ``python -m dbm`` +to identify, examine, and reorganize DBM database files. + +Command-line options +^^^^^^^^^^^^^^^^^^^^ + +.. option:: --whichdb file [file ...] + + Identify the database type for one or more database files: + + .. code-block:: shell-session + + $ python -m dbm --whichdb *.db + dbm.gnu - database1.db + dbm.sqlite3 - database2.db + UNKNOWN - corrupted.db + + This command uses the :func:`whichdb` function to determine the type + of each database file. Files that cannot be identified are marked as + ``UNKNOWN``. + +.. option:: --dump file + + Display the contents of a database file: + + .. code-block:: shell-session + + $ python -m dbm --dump mydb.db + username: john_doe + email: john@example.com + last_login: 2024-01-15 + + Keys and values are displayed in ``key: value`` format. Binary data + is decoded using UTF-8 with error replacement for display purposes. + +.. option:: --reorganize file + + Reorganize and compact a database file to reduce disk space: + + .. code-block:: shell-session + + $ python -m dbm --reorganize mydb.db + Reorganized database 'mydb.db' + + This operation uses the database's native :meth:`!reorganize` method + when available (:mod:`dbm.sqlite3`, :mod:`dbm.gnu`, :mod:`dbm.dumb`). + For database types that don't support reorganization, an error message + is displayed. + +.. option:: -h, --help + + Show the help message. diff --git a/Lib/dbm/__init__.py b/Lib/dbm/__init__.py index 4fdbc54e74cfb6..7bb78b846fbef3 100644 --- a/Lib/dbm/__init__.py +++ b/Lib/dbm/__init__.py @@ -32,7 +32,6 @@ import io import os import struct -import sys class error(Exception): @@ -187,8 +186,3 @@ def whichdb(filename): # Unknown return "" - - -if __name__ == "__main__": - for filename in sys.argv[1:]: - print(whichdb(filename) or "UNKNOWN", filename) diff --git a/Lib/dbm/__main__.py b/Lib/dbm/__main__.py new file mode 100644 index 00000000000000..79e8bb1a0fdd62 --- /dev/null +++ b/Lib/dbm/__main__.py @@ -0,0 +1,83 @@ +import argparse +import os +import sys + +from . import open as dbm_open, whichdb, error + + +def _whichdb_command(filenames): + exit_code = 0 + + for filename in filenames: + if os.path.exists(filename): + db_type = whichdb(filename) + print(f"{db_type or 'UNKNOWN'} {filename}") + else: + print(f"Error: File '{filename}' not found", file=sys.stderr) + exit_code = 1 + + return exit_code + + +def _dump_command(filename): + try: + with dbm_open(filename, "r") as db: + for key in db: + print(f"{key!r}: {db[key]!r}") + return 0 + except error: + print(f"Error: Database '{filename}' not found", file=sys.stderr) + return 1 + + +def _reorganize_command(filename): + try: + with dbm_open(filename, "c") as db: + if db.hasattr("reorganize"): + db.reorganize() + print(f"Reorganized database: '{filename}'", file=sys.stderr) + else: + print("Database type doesn't support reorganize method", + file=sys.stderr) + return 1 + return 0 + except error: + print(f"Error: Database '{filename}' not found or cannot be opened", + file=sys.stderr) + return 1 + + +def main(): + parser = argparse.ArgumentParser( + prog="python -m dbm", description="DBM toolkit" + ) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "--whichdb", + nargs="+", + metavar="file", + help="Identify database type for one or more files", + ) + group.add_argument( + "--dump", metavar="file", help="Display database contents" + ) + group.add_argument( + "--reorganize", + metavar="file", + help="Reorganize the database", + ) + options = parser.parse_args() + + try: + if options.whichdb: + return _whichdb_command(options.whichdb) + elif options.dump: + return _dump_command(options.dump) + elif options.reorganize: + return _reorganize_command(options.reorganize) + except KeyboardInterrupt: + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Lib/test/test_dbm.py b/Lib/test/test_dbm.py index ae9faabd536a6c..c6c51d9c624a49 100644 --- a/Lib/test/test_dbm.py +++ b/Lib/test/test_dbm.py @@ -1,10 +1,10 @@ -"""Test script for the dbm.open function based on testdumbdbm.py""" - import unittest import dbm import os + from test.support import import_helper from test.support import os_helper +from test.support.script_helper import assert_python_ok, assert_python_failure try: @@ -309,6 +309,94 @@ def setUp(self): self.dbm = import_helper.import_fresh_module('dbm') +class DBMCommandLineTestCase(unittest.TestCase): + + def setUp(self): + self.addCleanup(cleaunup_test_dir) + setup_test_dir() + self.test_db = os.path.join(dirname, 'test.db') + with dbm.open(self.test_db, 'c') as db: + db[b'key1'] = b'value1' + db[b'key2'] = b'value2' + self.empty_db = os.path.join(dirname, 'empty.db') + with dbm.open(self.empty_db, 'c'): + pass + self.dbm = import_helper.import_fresh_module('dbm') + + def run_cmd_ok(self, *args): + return assert_python_ok('-m', 'dbm', *args).out + + def run_cmd_error(self, *args): + return assert_python_failure('-m', 'dbm', *args) + + def test_help(self): + output = self.run_cmd_ok('-h') + self.assertIn(b'usage:', output) + self.assertIn(b'python -m dbm', output) + self.assertIn(b'--help', output) + self.assertIn(b'whichdb', output) + self.assertIn(b'dump', output) + self.assertIn(b'reorganize', output) + + def test_whichdb_command(self): + output = self.run_cmd_ok('--whichdb', self.test_db) + self.assertIn(self.test_db.encode(), output) + output = self.run_cmd_ok('--whichdb', self.test_db, self.empty_db) + self.assertIn(self.test_db.encode(), output) + self.assertIn(self.empty_db.encode(), output) + + def test_whichdb_nonexistent_file(self): + rc, _, stderr = self.run_cmd_error('--whichdb', "nonexistent_db") + self.assertEqual(rc, 1) + self.assertIn(b'not found', stderr) + + def test_whichdb_unknown_format(self): + text_file = os.path.join(dirname, 'text.txt') + with open(text_file, 'w') as f: + f.write('This is not a database file') + output = self.run_cmd_ok('--whichdb', text_file) + self.assertIn(b'UNKNOWN', output) + self.assertIn(text_file.encode(), output) + + def test_whichdb_output_format(self): + output = self.run_cmd_ok('--whichdb', self.test_db).decode() + self.assertIn(self.test_db, output) + + def test_dump_command(self): + output = self.run_cmd_ok('--dump', self.test_db) + self.assertIn(b"b'key1': b'value1'", output) + self.assertIn(b"b'key2': b'value2'", output) + + def test_dump_empty_database(self): + output = self.run_cmd_ok('--dump', self.empty_db) + self.assertEqual(output.strip(), b'') + + def test_dump_nonexistent_database(self): + rc, _, stderr = self.run_cmd_error('--dump', "nonexistent_db") + self.assertEqual(rc, 1) + self.assertIn(b'not found', stderr) + + def test_reorganize_command(self): + self.addCleanup(setattr, dbm, '_defaultmod', dbm._defaultmod) + for module in dbm_iterator(): + setup_test_dir() + dbm._defaultmod = module + with module.open(_fname, 'c') as f: + f[b"1"] = b"1" + if hasattr(module, 'reorganize'): + with module.open(_fname, 'c') as db: + output = self.run_cmd_ok('--reorganize', db) + self.assertIn(b'Reorganized', output) + + def test_output_format_consistency(self): + output = self.run_cmd_ok('--dump', self.test_db) + lines = output.decode().strip().split('\n') + for line in lines: + if line.strip(): # Skip empty lines + self.assertIn(':', line) + parts = line.split(':', 1) + self.assertEqual(len(parts), 2) + for mod in dbm_iterator(): assert mod.__name__.startswith('dbm.') suffix = mod.__name__[4:]