diff --git a/tests/test_list/cmd/test_end_to_end_list.py b/tests/test_list/cmd/test_end_to_end_list.py index 50ef97b3..1d2fea45 100644 --- a/tests/test_list/cmd/test_end_to_end_list.py +++ b/tests/test_list/cmd/test_end_to_end_list.py @@ -54,7 +54,7 @@ def test_help(self): self.assertEqual(reformat_help_message("""\ usage: trash-list [-h] [--print-completion {bash,zsh,tcsh}] [--version] [--volumes] [--trash-dirs] [--trash-dir TRASH_DIRS] - [--all-users] + [--all-users] [--sort {date,path,none}] List trashed files @@ -68,6 +68,9 @@ def test_help(self): --trash-dir TRASH_DIRS specify the trash directory to use --all-users list trashcans of all the users + --sort {date,path,none} + sort trashed files by date, path, or none (default: + 'none') Report bugs to https://github.com/andreafrancia/trash-cli/issues """), result.stderr + result.reformatted_help()) diff --git a/tests/test_list/cmd/test_trash_list.py b/tests/test_list/cmd/test_trash_list.py index 70f341e3..5d3b9ce5 100644 --- a/tests/test_list/cmd/test_trash_list.py +++ b/tests/test_list/cmd/test_trash_list.py @@ -1,5 +1,7 @@ # Copyright (C) 2011-2024 Andrea Francia Trivolzio(PV) Italy +import datetime + from tests.test_list.cmd.support.trash_list_user import trash_list_user user = trash_list_user @@ -74,3 +76,132 @@ def test_should_warn_about_unexistent_path_entry(self, user): "Parse Error: /xdg-data-home/Trash/info/foo.trashinfo: " "Unable to parse Path.\n", '') + + def test_sort_by_date(self, user): + user.home_trash_dir().add_trashinfo4('/file_b', "2000-01-01 00:00:03") + user.home_trash_dir().add_trashinfo4('/file_a', "2000-01-01 00:00:01") + user.home_trash_dir().add_trashinfo4('/file_c', "2000-01-01 00:00:02") + + output = user.run_trash_list('--sort=date') + + assert output.stdout == ( + "2000-01-01 00:00:01 /file_a\n" + "2000-01-01 00:00:02 /file_c\n" + "2000-01-01 00:00:03 /file_b\n") + + def test_sort_by_date_same_timestamp_sorts_by_path(self, user): + user.home_trash_dir().add_trashinfo4('/file_c', "2000-01-01 00:00:01") + user.home_trash_dir().add_trashinfo4('/file_a', "2000-01-01 00:00:01") + user.home_trash_dir().add_trashinfo4('/file_b', "2000-01-01 00:00:01") + + output = user.run_trash_list('--sort=date') + + assert output.stdout == ( + "2000-01-01 00:00:01 /file_a\n" + "2000-01-01 00:00:01 /file_b\n" + "2000-01-01 00:00:01 /file_c\n") + + def test_sort_by_path(self, user): + user.home_trash_dir().add_trashinfo4('/file_c', "2000-01-01 00:00:01") + user.home_trash_dir().add_trashinfo4('/file_a', "2000-01-01 00:00:03") + user.home_trash_dir().add_trashinfo4('/file_b', "2000-01-01 00:00:02") + + output = user.run_trash_list('--sort=path') + + assert output.stdout == ( + "2000-01-01 00:00:03 /file_a\n" + "2000-01-01 00:00:02 /file_b\n" + "2000-01-01 00:00:01 /file_c\n") + + def test_sort_by_path_same_path_sorts_by_date(self, user): + user.home_trash_dir().add_trashinfo4('/same_file', "2000-01-01 00:00:03") + user.home_trash_dir().add_trashinfo4('/same_file', "2000-01-01 00:00:01") + user.home_trash_dir().add_trashinfo4('/same_file', "2000-01-01 00:00:02") + + output = user.run_trash_list('--sort=path') + + assert output.stdout == ( + "2000-01-01 00:00:01 /same_file\n" + "2000-01-01 00:00:02 /same_file\n" + "2000-01-01 00:00:03 /same_file\n") + + def test_sort_none_lists_all_entries(self, user): + user.home_trash_dir().add_trashinfo4('/file_b', "2000-01-01 00:00:03") + user.home_trash_dir().add_trashinfo4('/file_a', "2000-01-01 00:00:01") + user.home_trash_dir().add_trashinfo4('/file_c', "2000-01-01 00:00:02") + + output = user.run_trash_list('--sort=none') + + assert output.all_lines() == {"2000-01-01 00:00:01 /file_a", + "2000-01-01 00:00:02 /file_c", + "2000-01-01 00:00:03 /file_b"} + + def test_sort_by_date_unknown_dates_sort_last(self, user): + user.home_trash_dir().add_trashinfo4('/known', "2000-01-01 00:00:01") + user.home_trash_dir().add_trashinfo_without_date('a-no-date') + user.home_trash_dir().add_trashinfo_wrong_date('b-bad-date', + 'Wrong date') + + output = user.run_trash_list('--sort=date') + + assert output.stdout == ( + "2000-01-01 00:00:01 /known\n" + "????-??-?? ??:??:?? /a-no-date\n" + "????-??-?? ??:??:?? /b-bad-date\n") + + def test_sort_by_path_with_unknown_dates(self, user): + user.home_trash_dir().add_trashinfo_without_date('z-no-date') + user.home_trash_dir().add_trashinfo4('/a-known', "2000-01-01 00:00:01") + + output = user.run_trash_list('--sort=path') + + assert output.stdout == ( + "2000-01-01 00:00:01 /a-known\n" + "????-??-?? ??:??:?? /z-no-date\n") + + def test_sort_with_parse_errors_keeps_errors_on_stderr(self, user): + user.home_trash_dir().add_trashinfo4('/file_b', "2000-01-01 00:00:02") + user.home_trash_dir().add_trashinfo4('/file_a', "2000-01-01 00:00:01") + user.home_trash_dir().add_trashinfo_content('broken', '') + + output = user.run_trash_list('--sort=date') + + assert output.stdout == ( + "2000-01-01 00:00:01 /file_a\n" + "2000-01-01 00:00:02 /file_b\n") + assert output.stderr == ( + "Parse Error: /xdg-data-home/Trash/info/broken.trashinfo: " + "Unable to parse Path.\n") + + def test_sort_by_path_with_files_flag(self, user): + user.home_trash_dir().add_trashinfo4('/file_b', "2000-01-01 00:00:01") + user.home_trash_dir().add_trashinfo4('/file_a', "2000-01-01 00:00:01") + + output = user.run_trash_list('--sort=path', '--files') + + lines = output.stdout.splitlines() + assert len(lines) == 2 + assert lines[0].startswith("2000-01-01 00:00:01 /file_a -> ") + assert lines[1].startswith("2000-01-01 00:00:01 /file_b -> ") + + def test_sort_by_path_with_size_flag(self, user): + user.home_trash_dir().add_trashed_file('big', '/file_a', 'X' * 10000) + user.home_trash_dir().add_trashed_file('small', '/file_b', 'X') + + output = user.run_trash_list('--size', '--sort=path') + + assert output.stdout == ( + "10000 /file_a\n" + "1 /file_b\n") + + def test_sort_by_date_with_size_flag(self, user): + user.home_trash_dir().add_trashed_file( + 'a', '/file_a', 'X' * 100, datetime.datetime(2000, 1, 1, 0, 0, 2)) + user.home_trash_dir().add_trashed_file( + 'b', '/file_b', 'X', datetime.datetime(2000, 1, 1, 0, 0, 1)) + + output = user.run_trash_list('--size', '--sort=date') + + assert output.stdout == ( + "1 /file_b\n" + "100 /file_a\n") diff --git a/tests/test_list/components/test_trash_list_parser.py b/tests/test_list/components/test_trash_list_parser.py index cfc45693..37f55813 100644 --- a/tests/test_list/components/test_trash_list_parser.py +++ b/tests/test_list/components/test_trash_list_parser.py @@ -45,5 +45,20 @@ def test_files_on(self): assert True == args.show_files + def test_sort_default(self): + args = self.parse([]) + + assert 'none' == args.sort + + def test_sort_date(self): + args = self.parse(['--sort=date']) + + assert 'date' == args.sort + + def test_sort_path(self): + args = self.parse(['--sort=path']) + + assert 'path' == args.sort + def parse(self, args): return self.parser.parse_list_args(args, 'trash-list') diff --git a/trashcli/list/list_trash_action.py b/trashcli/list/list_trash_action.py index 129d2e0e..d488cd38 100644 --- a/trashcli/list/list_trash_action.py +++ b/trashcli/list/list_trash_action.py @@ -9,6 +9,8 @@ from trashcli.lib.trash_dir_reader import TrashDirReader from trashcli.list.extractors import DeletionDateExtractor from trashcli.list.extractors import SizeExtractor +from trashcli.parse_trashinfo.maybe_parse_deletion_date import \ + maybe_parse_deletion_date from trashcli.parse_trashinfo.parse_path import parse_path from trashcli.parse_trashinfo.parser_error import ParseError from trashcli.trash_dirs_scanner import trash_dir_found @@ -24,6 +26,7 @@ class ListTrashArgs( ('attribute_to_print', str), ('show_files', bool), ('all_users', bool), + ('sort', str), ])): pass @@ -49,12 +52,26 @@ def __init__(self, def run_action(self, args, # type: ListTrashArgs ): - for message in ListTrash(self.environ, - self.uid, - self.selector, - self.dir_reader, - self.content_reader).list_all_trash(args): - self.print_event(message) + events = ListTrash(self.environ, + self.uid, + self.selector, + self.dir_reader, + self.content_reader).list_all_trash(args) + if args.sort != 'none': + events = list(events) + output_positions = [i for i, e in enumerate(events) + if isinstance(e, Output)] + outputs = [events[i] for i in output_positions] + if args.sort == 'date': + outputs.sort(key=lambda e: (e.deletion_date, + e.original_location)) + elif args.sort == 'path': + outputs.sort(key=lambda e: (e.original_location, + e.deletion_date)) + for pos, sorted_output in zip(output_positions, outputs): + events[pos] = sorted_output + for event in events: + self.print_event(event) def print_event(self, event): if isinstance(event, Error): @@ -128,6 +145,7 @@ def _print_trashinfo(self, else: attribute = extractor.extract_attribute(trashinfo_path, contents) + deletion_date = "%s" % maybe_parse_deletion_date(contents) original_location = os.path.join(volume, relative_location) if show_files: @@ -136,7 +154,7 @@ def _print_trashinfo(self, original_file) else: line = format_line(attribute, original_location) - yield Output(line) + yield Output(line, deletion_date, original_location) def top_trashdir_skipped_because_parent_is_symlink(self, trashdir): return "TrashDir skipped because parent is symlink: %s" % trashdir @@ -158,8 +176,10 @@ def __init__(self, error): class Output(Event): - def __init__(self, message): + def __init__(self, message, deletion_date, original_location): self.message = message + self.deletion_date = deletion_date + self.original_location = original_location def format_line(attribute, original_location): diff --git a/trashcli/list/parser.py b/trashcli/list/parser.py index bd6ef296..16f8bf81 100644 --- a/trashcli/list/parser.py +++ b/trashcli/list/parser.py @@ -69,6 +69,12 @@ def __init__(self, prog): action='store_true', dest='all_users', help='list trashcans of all the users') + self.parser.add_argument('--sort', + choices=['date', 'path', 'none'], + default='none', + dest='sort', + help="sort trashed files by date, path, " + "or none (default: 'none')") self.parser.add_argument('--python', dest='action', action='store_const', @@ -98,7 +104,8 @@ def parse_list_args(self, trash_dirs=parsed.trash_dirs, attribute_to_print=parsed.attribute_to_print, show_files=parsed.show_files, - all_users=parsed.all_users + all_users=parsed.all_users, + sort=parsed.sort ) if parsed.action == ListAction.print_python_executable: return PrintPythonExecutableArgs()