those who've otherwise worked on that item. For a list of
contributors names and identifiers please see the CONTRIBUTORS file.
+Changes from 2.0.0 to 2.1.0:
+
+ - Rename cs command ("clear all selections") from cs to csel.
+ - Rename igs command ("invert selections") from igs to isel.
+ - Add "prune" command as synonym for DELETE key.
+ - Clear selections after deleting files via prune/DELETE.
+ - Fix dependency issues when fdupes is configured to not use ncurses.
+
+Changes from 1.6.1 to 2.0.0:
+
+- Add ncurses mode for interactive file deletion (plain mode still available
+ via --plain or ./configure).
+- Add --minsize option.
+- Add --maxsize option.
+- Add --time option.
+- Add --order=ctime option.
+- Add --log option.
+- Use configure script for installation (Autotools/Automake).
+
Changes from 1.6.0 to 1.6.1:
- Fix 'invalid option' error for -I. [AL]
flags.h\
mbstowcs_escape_invalid.c\
mbstowcs_escape_invalid.h\
- positive_wcwidth.c\
- positive_wcwidth.h\
md5/md5.c\
md5/md5.h
dist_man1_MANS = fdupes.1
-H --hardlinks normally, when two or more files point to the same
disk area they are treated as non-duplicates; this
option will change this behavior
- -G --minsize=SIZE consider only files greater than or equal to SIZE
- -L --maxsize=SIZE consider only files less than or equal to SIZE
+ -G --minsize=SIZE consider only files greater than or equal to SIZE in bytes
+ -L --maxsize=SIZE consider only files less than or equal to SIZE in bytes
-n --noempty exclude zero-length files from consideration
-A --nohidden exclude hidden files from consideration
-f --omitfirst omit the first file in each set of matches
-AC_INIT([fdupes], [2.0.0])
+AC_INIT([fdupes], [2.1.0])
AM_INIT_AUTOMAKE([foreign subdir-objects])
(see below).
.B
-.IP "'cs'"
+.IP "'csel'"
Clear all selections.
.B
-.IP "'igs'"
+.IP "'isel'"
Invert selections within selected sets. For example, if files 1 and 4 in a set of 5 are selected,
.B
-igs
+isel
will deselect files 1 and 4, and select files 2, 3, and 5. Immediately repeating the same command will deselect files 2, 3, and 5, and select files 1 and 4, restoring selections to their previous state.
.SH "TAGGING SELECTED FILES"
Remove all tags from selected files.
.SH "DELETING DUPLICATES"
-Once they've been tagged for deletion, files can be deleted by pressing
+Once tagged for deletion, files can be deleted by pressing
.B
-DELETE.
-Fdupes will delete any files that are tagged for deletion and delist any sets whose remaining files have been tagged for keeping. For safety, fdupes will refuse to act on sets for which all files have been tagged for deletion. To handle these cases, tag at least one file for keeping and run the delete command again.
+DELETE
+or using the
+.B 'prune'
+command. Fdupes will delete any files that are tagged for deletion and delist any sets whose remaining files have been tagged for keeping. For safety, fdupes will refuse to act on sets for which all files have been tagged for deletion. To handle these cases, tag at least one file for keeping and run the delete command again.
.SH "OTHER COMMANDS"
.B
#include <errno.h>
#include <libgen.h>
#include <locale.h>
+#ifndef NO_NCURSES
#ifdef HAVE_NCURSESW_CURSES_H
#include <ncursesw/curses.h>
#else
#include <curses.h>
#endif
+#include "ncurses-interface.h"
+#endif
#include "fdupes.h"
#include "errormsg.h"
-#include "ncurses-interface.h"
#include "log.h"
#include "sigint.h"
#include "flags.h"
#include "ncurses-commands.h"
#include "wcs.h"
#include "mbstowcs_escape_invalid.h"
+#include "log.h"
#include <wchar.h>
#include <pcre2.h>
{L"dsele", COMMAND_CLEAR_SELECTIONS_ENDING},
{L"dselm", COMMAND_CLEAR_SELECTIONS_MATCHING},
{L"dselr", COMMAND_CLEAR_SELECTIONS_REGEX},
- {L"cs", COMMAND_CLEAR_ALL_SELECTIONS},
- {L"igs", COMMAND_INVERT_GROUP_SELECTIONS},
+ {L"csel", COMMAND_CLEAR_ALL_SELECTIONS},
+ {L"isel", COMMAND_INVERT_GROUP_SELECTIONS},
{L"ks", COMMAND_KEEP_SELECTED},
{L"ds", COMMAND_DELETE_SELECTED},
{L"rs", COMMAND_RESET_SELECTED},
{L"rg", COMMAND_RESET_GROUP},
{L"all", COMMAND_PRESERVE_ALL},
{L"goto", COMMAND_GOTO_SET},
+ {L"prune", COMMAND_PRUNE},
{L"exit", COMMAND_EXIT},
{L"quit", COMMAND_EXIT},
{L"help", COMMAND_HELP},
groups[g].selected = 0;
}
- format_status_left(status, L"Cleared all selections.");
+ if (status)
+ format_status_left(status, L"Cleared all selections.");
return 1;
}
return 1;
}
+
+int filerowcount(file_t *file, const int columns, int group_file_count);
+
+/* delete files tagged for deletion, delist sets with no untagged files */
+int cmd_prune(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, int *totalgroups, int *cursorgroup, int *cursorfile, int *topline, char *logfile, WINDOW *filewin, struct status_text *status)
+{
+ int deletecount;
+ int preservecount;
+ int unresolvedcount;
+ int totaldeleted = 0;
+ double deletedbytes = 0;
+ struct log_info *loginfo;
+ int g;
+ int f;
+ int to;
+ int adjusttopline;
+ int toplineoffset;
+ int groupfirstline;
+
+ if (logfile != 0)
+ loginfo = log_open(logfile, 0);
+ else
+ loginfo = 0;
+
+ for (g = 0; g < *totalgroups; ++g)
+ {
+ preservecount = 0;
+ deletecount = 0;
+ unresolvedcount = 0;
+
+ for (f = 0; f < groups[g].filecount; ++f)
+ {
+ switch (groups[g].files[f].action)
+ {
+ case -1:
+ ++deletecount;
+ break;
+ case 0:
+ ++unresolvedcount;
+ break;
+ case 1:
+ ++preservecount;
+ break;
+ }
+ }
+
+ if (loginfo)
+ log_begin_set(loginfo);
+
+ /* delete files marked for deletion unless no files left undeleted */
+ if (deletecount < groups[g].filecount)
+ {
+ for (f = 0; f < groups[g].filecount; ++f)
+ {
+ if (groups[g].files[f].action == -1)
+ {
+ if (remove(groups[g].files[f].file->d_name) == 0)
+ {
+ set_file_action(&groups[g].files[f], -2, deletiontally);
+
+ deletedbytes += groups[g].files[f].file->size;
+ ++totaldeleted;
+
+ if (loginfo)
+ log_file_deleted(loginfo, groups[g].files[f].file->d_name);
+ }
+ }
+ }
+
+ if (loginfo)
+ {
+ for (f = 0; f < groups[g].filecount; ++f)
+ {
+ if (groups[g].files[f].action >= 0)
+ log_file_remaining(loginfo, groups[g].files[f].file->d_name);
+ }
+ }
+
+ deletecount = 0;
+ }
+
+ if (loginfo)
+ log_end_set(loginfo);
+
+ /* if no files left unresolved, mark preserved files for delisting */
+ if (unresolvedcount == 0)
+ {
+ for (f = 0; f < groups[g].filecount; ++f)
+ if (groups[g].files[f].action == 1)
+ set_file_action(&groups[g].files[f], -2, deletiontally);
+
+ preservecount = 0;
+ }
+ /* if only one file left unresolved, mark it for delesting */
+ else if (unresolvedcount == 1 && preservecount + deletecount == 0)
+ {
+ for (f = 0; f < groups[g].filecount; ++f)
+ if (groups[g].files[f].action == 0)
+ set_file_action(&groups[g].files[f], -2, deletiontally);
+ }
+
+ /* delist any files marked for delisting */
+ to = 0;
+ for (f = 0; f < groups[g].filecount; ++f)
+ if (groups[g].files[f].action != -2)
+ groups[g].files[to++] = groups[g].files[f];
+
+ groups[g].filecount = to;
+
+ /* reposition cursor, if necessary */
+ if (*cursorgroup == g && *cursorfile > 0 && *cursorfile >= groups[g].filecount)
+ *cursorfile = groups[g].filecount - 1;
+ }
+
+ if (loginfo != 0)
+ log_close(loginfo);
+
+ if (deletedbytes < 1000.0)
+ format_status_left(status, L"Deleted %ld files (occupying %.0f bytes).", totaldeleted, deletedbytes);
+ else if (deletedbytes <= (1000.0 * 1000.0))
+ format_status_left(status, L"Deleted %ld files (occupying %.1f KB).", totaldeleted, deletedbytes / 1000.0);
+ else if (deletedbytes <= (1000.0 * 1000.0 * 1000.0))
+ format_status_left(status, L"Deleted %ld files (occupying %.1f MB).", totaldeleted, deletedbytes / (1000.0 * 1000.0));
+ else
+ format_status_left(status, L"Deleted %ld files (occupying %.1f GB).", totaldeleted, deletedbytes / (1000.0 * 1000.0 * 1000.0));
+
+ /* delist empty groups */
+ to = 0;
+ for (g = 0; g < *totalgroups; ++g)
+ {
+ if (groups[g].filecount > 0)
+ {
+ groups[to] = groups[g];
+
+ /* reposition cursor, if necessary */
+ if (to == *cursorgroup && to != g)
+ *cursorfile = 0;
+
+ ++to;
+ }
+ else
+ {
+ free(groups[g].files);
+ }
+ }
+
+ *totalgroups = to;
+
+ /* reposition cursor, if necessary */
+ if (*cursorgroup >= *totalgroups)
+ {
+ *cursorgroup = *totalgroups - 1;
+ *cursorfile = 0;
+ }
+
+ /* recalculate line boundaries */
+ adjusttopline = 1;
+ toplineoffset = 0;
+ groupfirstline = 0;
+
+ for (g = 0; g < *totalgroups; ++g)
+ {
+ if (adjusttopline && groups[g].endline >= *topline)
+ toplineoffset = groups[g].endline - *topline;
+
+ groups[g].startline = groupfirstline;
+ groups[g].endline = groupfirstline + 2;
+
+ for (f = 0; f < groups[g].filecount; ++f)
+ groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
+
+ if (adjusttopline && toplineoffset > 0)
+ {
+ *topline = groups[g].endline - toplineoffset;
+
+ if (*topline < 0)
+ *topline = 0;
+
+ adjusttopline = 0;
+ }
+
+ groupfirstline = groups[g].endline + 1;
+ }
+
+ if (*totalgroups > 0 && groups[*totalgroups-1].endline <= *topline)
+ {
+ *topline = groups[*totalgroups-1].endline - getmaxy(filewin) + 1;
+
+ if (*topline < 0)
+ *topline = 0;
+ }
+
+ cmd_clear_all_selections(groups, *totalgroups, commandarguments, 0);
+}
\ No newline at end of file
#define COMMAND_SELECT_REGEX 20
#define COMMAND_CLEAR_SELECTIONS_REGEX 21
#define COMMAND_GOTO_SET 22
+#define COMMAND_PRUNE 23
extern struct command_map command_list[];
extern struct command_map confirmation_keyword_list[];
int cmd_keep_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status);
int cmd_delete_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status);
int cmd_reset_selected(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, struct status_text *status);
+int cmd_prune(struct filegroup *groups, int groupcount, wchar_t *commandarguments, size_t *deletiontally, int *totalgroups, int *cursorgroup, int *cursorfile, int *topline, char *logfile, WINDOW *filewin, struct status_text *status);;
#endif
\ No newline at end of file
int preservecount;
int deletecount;
int unresolvedcount;
- int totaldeleted;
size_t globaldeletiontally = 0;
- double deletedbytes;
int row;
int x;
int g;
int keyresult;
int cy;
int f;
- int to;
wchar_t *commandbuffer;
size_t commandbuffersize;
wchar_t *commandarguments;
struct prompt_info *prompt;
int dupesfound;
int intresult;
- int adjusttopline;
- int toplineoffset = 0;
int resumecommandinput = 0;
int index_width;
int timestamp_width;
- struct log_info *loginfo;
noecho();
cbreak();
break;
+ case COMMAND_PRUNE:
+ cmd_prune(groups, totalgroups, commandarguments, &globaldeletiontally, &totalgroups, &cursorgroup, &cursorfile, &topline, logfile, filewin, status);
+ break;
+
case COMMAND_EXIT: /* exit program */
if (totalgroups == 0)
{
break;
case KEY_DC:
- totaldeleted = 0;
- deletedbytes = 0;
-
- if (logfile != 0)
- loginfo = log_open(logfile, 0);
- else
- loginfo = 0;
-
- for (g = 0; g < totalgroups; ++g)
- {
- preservecount = 0;
- deletecount = 0;
- unresolvedcount = 0;
-
- for (f = 0; f < groups[g].filecount; ++f)
- {
- switch (groups[g].files[f].action)
- {
- case -1:
- ++deletecount;
- break;
- case 0:
- ++unresolvedcount;
- break;
- case 1:
- ++preservecount;
- break;
- }
- }
-
- if (loginfo)
- log_begin_set(loginfo);
-
- /* delete files marked for deletion unless no files left undeleted */
- if (deletecount < groups[g].filecount)
- {
- for (f = 0; f < groups[g].filecount; ++f)
- {
- if (groups[g].files[f].action == -1)
- {
- if (remove(groups[g].files[f].file->d_name) == 0)
- {
- set_file_action(&groups[g].files[f], -2, &globaldeletiontally);
-
- deletedbytes += groups[g].files[f].file->size;
- ++totaldeleted;
-
- if (loginfo)
- log_file_deleted(loginfo, groups[g].files[f].file->d_name);
- }
- }
- }
-
- if (loginfo)
- {
- for (f = 0; f < groups[g].filecount; ++f)
- {
- if (groups[g].files[f].action >= 0)
- log_file_remaining(loginfo, groups[g].files[f].file->d_name);
- }
- }
-
- deletecount = 0;
- }
-
- if (loginfo)
- log_end_set(loginfo);
-
- /* if no files left unresolved, mark preserved files for delisting */
- if (unresolvedcount == 0)
- {
- for (f = 0; f < groups[g].filecount; ++f)
- if (groups[g].files[f].action == 1)
- set_file_action(&groups[g].files[f], -2, &globaldeletiontally);
-
- preservecount = 0;
- }
- /* if only one file left unresolved, mark it for delesting */
- else if (unresolvedcount == 1 && preservecount + deletecount == 0)
- {
- for (f = 0; f < groups[g].filecount; ++f)
- if (groups[g].files[f].action == 0)
- set_file_action(&groups[g].files[f], -2, &globaldeletiontally);
- }
-
- /* delist any files marked for delisting */
- to = 0;
- for (f = 0; f < groups[g].filecount; ++f)
- if (groups[g].files[f].action != -2)
- groups[g].files[to++] = groups[g].files[f];
-
- groups[g].filecount = to;
-
- /* reposition cursor, if necessary */
- if (cursorgroup == g && cursorfile > 0 && cursorfile >= groups[g].filecount)
- cursorfile = groups[g].filecount - 1;
- }
-
- if (loginfo != 0)
- log_close(loginfo);
-
- if (deletedbytes < 1000.0)
- format_status_left(status, L"Deleted %ld files (occupying %.0f bytes).", totaldeleted, deletedbytes);
- else if (deletedbytes <= (1000.0 * 1000.0))
- format_status_left(status, L"Deleted %ld files (occupying %.1f KB).", totaldeleted, deletedbytes / 1000.0);
- else if (deletedbytes <= (1000.0 * 1000.0 * 1000.0))
- format_status_left(status, L"Deleted %ld files (occupying %.1f MB).", totaldeleted, deletedbytes / (1000.0 * 1000.0));
- else
- format_status_left(status, L"Deleted %ld files (occupying %.1f GB).", totaldeleted, deletedbytes / (1000.0 * 1000.0 * 1000.0));
-
- /* delist empty groups */
- to = 0;
- for (g = 0; g < totalgroups; ++g)
- {
- if (groups[g].filecount > 0)
- {
- groups[to] = groups[g];
-
- /* reposition cursor, if necessary */
- if (to == cursorgroup && to != g)
- cursorfile = 0;
-
- ++to;
- }
- else
- {
- free(groups[g].files);
- }
- }
-
- totalgroups = to;
-
- /* reposition cursor, if necessary */
- if (cursorgroup >= totalgroups)
- {
- cursorgroup = totalgroups - 1;
- cursorfile = 0;
- }
-
- /* recalculate line boundaries */
- adjusttopline = 1;
- toplineoffset = 0;
- groupfirstline = 0;
-
- for (g = 0; g < totalgroups; ++g)
- {
- if (adjusttopline && groups[g].endline >= topline)
- toplineoffset = groups[g].endline - topline;
-
- groups[g].startline = groupfirstline;
- groups[g].endline = groupfirstline + 2;
-
- for (f = 0; f < groups[g].filecount; ++f)
- groups[g].endline += filerowcount(groups[g].files[f].file, COLS, groups[g].filecount);
-
- if (adjusttopline && toplineoffset > 0)
- {
- topline = groups[g].endline - toplineoffset;
-
- if (topline < 0)
- topline = 0;
-
- adjusttopline = 0;
- }
-
- groupfirstline = groups[g].endline + 1;
- }
-
- if (totalgroups > 0 && groups[totalgroups-1].endline <= topline)
- {
- topline = groups[totalgroups-1].endline - getmaxy(filewin) + 1;
-
- if (topline < 0)
- topline = 0;
- }
-
+ cmd_prune(groups, totalgroups, commandarguments, &globaldeletiontally, &totalgroups, &cursorgroup, &cursorfile, &topline, logfile, filewin, status);
break;
case KEY_RESIZE: