Converted README to markdown
[rox-filer.git] / ROX-Filer / src / bulk_rename.c
blobb87f158568b4cff477cb1a933a31af52f3b41324
1 /*
2 * ROX-Filer, filer for the ROX desktop project
3 * Copyright (C) 2006, Thomas Leonard and others (see changelog for details).
5 * This program is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License as published by the Free
7 * Software Foundation; either version 2 of the License, or (at your option)
8 * any later version.
10 * This program is distributed in the hope that it will be useful, but WITHOUT
11 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 * more details.
15 * You should have received a copy of the GNU General Public License along with
16 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
17 * Place, Suite 330, Boston, MA 02111-1307 USA
20 /* bulk_rename.c - rename multiple files at once */
22 #include "config.h"
24 #include <stdlib.h>
25 #include <gtk/gtk.h>
26 #include <sys/types.h>
27 #include <regex.h>
28 #include <string.h>
29 #include <errno.h>
31 #include "global.h"
33 #include "main.h"
34 #include "bulk_rename.h"
35 #include "support.h"
36 #include "gui_support.h"
38 enum {RESPONSE_RENAME, RESPONSE_RESET};
40 /* Static prototypes */
41 static gboolean apply_replace(GtkWidget *box);
42 static void response(GtkWidget *box, int resp, GtkListStore *model);
43 static void reset_model(GtkListStore *model);
44 static gboolean rename_items(const char *dir, GtkListStore *list);
45 static void cell_edited(GtkCellRendererText *cell, const gchar *path_string,
46 const gchar *new_text, GtkTreeModel *model);
49 /****************************************************************
50 * EXTERNAL INTERFACE *
51 ****************************************************************/
53 /* Bulk rename these items */
54 void bulk_rename(const char *dir, GList *items)
56 GtkWidget *box, *button, *tree, *swin, *hbox;
57 GtkWidget *replace_entry, *with_entry;
58 GtkTreeViewColumn *column;
59 GtkCellRenderer *cell_renderer;
60 GtkListStore *model;
61 GtkRequisition req;
63 box = gtk_dialog_new();
64 g_object_set_data_full(G_OBJECT(box), "rename_dir",
65 g_strdup(dir), g_free);
66 gtk_window_set_title(GTK_WINDOW(box), _("Bulk rename files"));
67 gtk_dialog_set_has_separator(GTK_DIALOG(box), FALSE);
69 button = button_new_mixed(GTK_STOCK_REFRESH, _("Reset"));
70 GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
71 gtk_dialog_add_action_widget(GTK_DIALOG(box), button, RESPONSE_RESET);
72 gtk_dialog_set_default_response(GTK_DIALOG(box), RESPONSE_RESET);
73 gtk_tooltips_set_tip(tooltips, button,
74 _("Make the New column a copy of Old"), NULL);
76 gtk_dialog_add_button(GTK_DIALOG(box),
77 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL);
79 button = button_new_mixed(GTK_STOCK_EXECUTE, _("_Rename"));
80 GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
81 gtk_dialog_add_action_widget(GTK_DIALOG(box), button, RESPONSE_RENAME);
82 gtk_dialog_set_default_response(GTK_DIALOG(box), RESPONSE_RENAME);
84 /* Replace */
86 hbox = gtk_hbox_new(FALSE, 4);
87 gtk_container_set_border_width(GTK_CONTAINER(hbox), 4);
88 gtk_box_pack_start(GTK_BOX(GTK_DIALOG(box)->vbox),
89 hbox, FALSE, TRUE, 0);
91 gtk_box_pack_start(GTK_BOX(hbox),
92 gtk_label_new(_("Replace:")), FALSE, TRUE, 0);
94 replace_entry = gtk_entry_new();
95 g_object_set_data(G_OBJECT(box), "replace_entry", replace_entry);
96 gtk_box_pack_start(GTK_BOX(hbox), replace_entry, TRUE, TRUE, 0);
97 gtk_entry_set_text(GTK_ENTRY(replace_entry), "\\.htm$");
98 gtk_tooltips_set_tip(tooltips, replace_entry,
99 _("This is a regular expression to search for.\n"
100 "^ matches the start of a filename\n"
101 "$ matches the end\n"
102 "\\. matches a dot\n"
103 "\\.htm$ matches the '.htm' in 'index.htm', etc"),
104 NULL);
106 gtk_box_pack_start(GTK_BOX(hbox),
107 gtk_label_new(_("With:")), FALSE, TRUE, 0);
109 with_entry = gtk_entry_new();
110 g_object_set_data(G_OBJECT(box), "with_entry", with_entry);
111 gtk_box_pack_start(GTK_BOX(hbox), with_entry, TRUE, TRUE, 0);
112 gtk_entry_set_text(GTK_ENTRY(with_entry), ".html");
113 gtk_tooltips_set_tip(tooltips, with_entry,
114 _("The first match in each filename will be replaced "
115 "by this string. "
116 "The only special characters are back-references "
117 "from \\0 to \\9. To use them literally, "
118 "they have to be escaped with a backslash."), NULL);
120 button = gtk_button_new_with_label(_("Apply"));
121 gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, TRUE, 0);
122 gtk_tooltips_set_tip(tooltips, button,
123 _("Do a search-and-replace in the New column. "
124 "The files are not actually renamed until you click "
125 "on the Rename button below."), NULL);
127 g_signal_connect_swapped(replace_entry, "activate",
128 G_CALLBACK(gtk_widget_grab_focus), with_entry);
129 g_signal_connect_swapped(with_entry, "activate",
130 G_CALLBACK(apply_replace), box);
131 g_signal_connect_swapped(button, "clicked",
132 G_CALLBACK(apply_replace), box);
134 /* The TreeView */
136 model = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
137 g_object_set_data(G_OBJECT(box), "tree_model", model);
138 tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(model));
140 cell_renderer = gtk_cell_renderer_text_new();
141 column = gtk_tree_view_column_new_with_attributes(
142 _("Old name"), cell_renderer, "text", 0, NULL);
143 gtk_tree_view_column_set_resizable(column, TRUE);
144 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
146 cell_renderer = gtk_cell_renderer_text_new();
147 g_object_set(G_OBJECT(cell_renderer), "editable", TRUE, NULL);
148 g_signal_connect(G_OBJECT(cell_renderer), "edited",
149 G_CALLBACK(cell_edited), model);
150 column = gtk_tree_view_column_new_with_attributes(
151 _("New name"), cell_renderer, "text", 1, NULL);
152 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
154 swin = gtk_scrolled_window_new(NULL, NULL);
155 gtk_container_set_border_width(GTK_CONTAINER(swin), 4);
156 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(swin),
157 GTK_SHADOW_IN);
158 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swin),
159 GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
160 gtk_box_pack_start(GTK_BOX(GTK_DIALOG(box)->vbox), swin, TRUE, TRUE, 0);
161 gtk_container_add(GTK_CONTAINER(swin), tree);
163 while (items) {
164 GtkTreeIter iter;
165 const char *name = items->data;
167 gtk_list_store_append(model, &iter);
168 gtk_list_store_set(model, &iter, 0, name, 1, name, -1);
170 items = items->next;
173 gtk_widget_show_all(tree);
174 gtk_widget_size_request(tree, &req);
175 req.width = MIN(req.width + 50, screen_width - 50);
176 req.height = MIN(req.height + 150, screen_height - 50);
178 gtk_window_set_default_size(GTK_WINDOW(box), req.width, req.height);
180 number_of_windows++;
181 g_signal_connect(box, "destroy", G_CALLBACK(one_less_window), NULL);
182 g_signal_connect(box, "response", G_CALLBACK(response), model);
183 gtk_widget_show_all(box);
186 /****************************************************************
187 * INTERNAL FUNCTIONS *
188 ****************************************************************/
190 static void response(GtkWidget *box, int resp, GtkListStore *model)
192 if (resp == RESPONSE_RESET)
193 reset_model(model);
194 else if (resp == RESPONSE_RENAME)
196 if (rename_items(g_object_get_data(G_OBJECT(box), "rename_dir"),
197 model))
198 gtk_widget_destroy(box);
200 else
201 gtk_widget_destroy(box);
204 /** Substitute: s/old/with/
205 * Returns the result as a new string, or NULL if there is no match.
206 * "replace" is a compiled version of "with".
207 * Caller must free the result.
209 static GString *subst(const char *old, regex_t *replace, const char *with)
211 int max_subs = 10;
212 GString *new;
213 regmatch_t match[max_subs];
215 if (regexec(replace, old, max_subs, match, 0) != 0)
216 return NULL; /* No match */
218 g_return_val_if_fail(match[0].rm_so != -1, NULL);
220 new = g_string_new(NULL);
221 g_string_append_len(new, old, match[0].rm_so);
223 int i;
224 for (i = 0; with[i]; i++)
226 if (with[i] == '\\' && with[i+1])
228 i++;
229 if (with[i] >= '0' && with[i]-'0' < max_subs)
231 int subpat;
233 subpat = with[i] - '0';
235 if (match[subpat].rm_so != -1)
236 g_string_append_len(new, old + match[subpat].rm_so,
237 match[subpat].rm_eo - match[subpat].rm_so);
240 else
242 // Escape next character
243 g_string_append_c(new, with[i]);
246 else
247 g_string_append_c(new, with[i]);
250 g_string_append(new, old + match[0].rm_eo);
252 return new;
255 /* Do a search-and-replace on the second column. */
256 static void update_model(GtkListStore *list, regex_t *replace, const char *with)
258 GtkTreeIter iter;
259 GtkTreeModel *model = (GtkTreeModel *) list;
260 int n_matched = 0;
261 int n_changed = 0;
263 if (!gtk_tree_model_get_iter_first(model, &iter))
265 g_warning("Model empty!");
266 return;
271 GString *new;
272 char *old = NULL;
274 gtk_tree_model_get(model, &iter, 1, &old, -1);
276 new = subst(old, replace, with);
277 if (new)
279 n_matched++;
280 if (strcmp(old, new->str) != 0)
282 n_changed++;
283 gtk_list_store_set(list, &iter, 1, new->str, -1);
286 g_string_free(new, TRUE);
288 g_free(old);
290 } while (gtk_tree_model_iter_next(model, &iter));
292 if (n_matched == 0)
293 report_error(_("No strings (in the New column) matched "
294 "the given expression"));
295 else if (n_changed == 0)
297 if (n_matched == 1)
298 report_error(_("One name matched, but the result was "
299 "the same"));
300 else
301 report_error(_("%d names matched, but the results were "
302 "all the same"), n_matched);
306 static gboolean apply_replace(GtkWidget *box)
308 GtkListStore *model;
309 GtkEntry *replace_entry, *with_entry;
310 const char *replace, *with;
311 regex_t compiled;
312 int error;
314 replace_entry = g_object_get_data(G_OBJECT(box), "replace_entry");
315 with_entry = g_object_get_data(G_OBJECT(box), "with_entry");
316 model = g_object_get_data(G_OBJECT(box), "tree_model");
318 g_return_val_if_fail(replace_entry != NULL, TRUE);
319 g_return_val_if_fail(with_entry != NULL, TRUE);
320 g_return_val_if_fail(model != NULL, TRUE);
322 replace = gtk_entry_get_text(replace_entry);
323 with = gtk_entry_get_text(with_entry);
325 if (replace[0] == '\0' && with[0] == '\0')
327 report_error(_("Specify a regular expression to match, "
328 "and a string to replace matches with."));
329 return TRUE;
332 error = regcomp(&compiled, replace, REG_EXTENDED);
333 if (error)
335 char *message;
336 size_t size;
338 size = regerror(error, &compiled, NULL, 0);
339 g_return_val_if_fail(size > 0, TRUE);
341 message = g_malloc(size);
342 regerror(error, &compiled, message, size);
344 report_error(_("%s (for '%s')"), message, replace);
346 return TRUE;
349 update_model(model, &compiled, with);
351 regfree(&compiled);
353 return TRUE;
356 static void reset_model(GtkListStore *list)
358 GtkTreeIter iter;
359 GtkTreeModel *model = (GtkTreeModel *) list;
361 if (!gtk_tree_model_get_iter_first(model, &iter))
362 return;
364 do {
365 char *before;
366 gtk_tree_model_get(model, &iter, 0, &before, -1);
367 gtk_list_store_set(list, &iter, 1, before, -1);
368 g_free(before);
369 } while (gtk_tree_model_iter_next(model, &iter));
372 static gboolean do_rename(const char *before, const char *after)
374 /* Check again, just in case */
375 if (access(after, F_OK) == 0)
377 report_error(_("A file called '%s' already exists. "
378 "Aborting bulk rename."), after);
380 else if (rename(before, after))
382 report_error(_("Failed to rename '%s' as '%s':\n%s\n"
383 "Aborting bulk rename."), before, after,
384 g_strerror(errno));
386 else
387 return TRUE;
388 return FALSE;
391 static gboolean rename_items(const char *dir, GtkListStore *list)
393 GtkTreeModel *model = (GtkTreeModel *) list;
394 GtkTreeIter iter;
395 char *slash_example = NULL;
396 GHashTable *names = NULL;
397 gboolean success = FALSE;
398 int n_renames = 0;
400 g_return_val_if_fail(dir != NULL, FALSE);
401 g_return_val_if_fail(list != NULL, FALSE);
403 if (!gtk_tree_model_get_iter_first(model, &iter))
404 return FALSE; /* (error) */
406 names = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
407 do {
408 char *before, *after;
409 const char *dest;
411 gtk_tree_model_get(model, &iter, 0, &before, 1, &after, -1);
413 if (!slash_example && strchr(after, '/'))
415 slash_example = g_strdup(after);
418 if (g_hash_table_lookup(names, before))
420 report_error("Filename '%s' used twice!", before);
421 goto fail;
423 g_hash_table_insert(names, before, "");
425 if (after[0] == '\0' || strcmp(after, before) == 0)
427 g_free(after);
428 continue;
431 if (g_hash_table_lookup(names, after))
433 report_error("Filename '%s' used twice!", after);
434 goto fail;
436 g_hash_table_insert(names, after, "");
438 if (after[0] == '/')
439 dest = after;
440 else
441 dest = make_path(dir, after);
442 if (access(dest, F_OK) == 0)
444 report_error(_("A file called '%s' already exists"),
445 dest);
446 goto fail;
449 n_renames++;
450 } while (gtk_tree_model_iter_next(model, &iter));
452 if (slash_example)
454 char *message;
455 message = g_strdup_printf(_("Some of the New names contain "
456 "/ characters (eg '%s'). "
457 "This will cause the files to end up in "
458 "different directories. "
459 "Continue?"), slash_example);
460 if (!confirm(message, GTK_STOCK_EXECUTE, "Rename anyway"))
462 g_free(message);
463 goto fail;
465 g_free(message);
468 if (n_renames == 0)
470 report_error(_("None of the names have changed. "
471 "Nothing to do!"));
472 goto fail;
475 success = TRUE;
476 gtk_tree_model_get_iter_first(model, &iter);
477 while (success)
479 char *before, *after, *before_path;
480 const char *dest;
482 gtk_tree_model_get(model, &iter, 0, &before, 1, &after, -1);
484 if (after[0] == '\0' || strcmp(after, before) == 0)
485 dest = NULL;
486 else if (after[0] == '/')
487 dest = after;
488 else
489 dest = make_path(dir, after);
491 before_path = g_build_filename(dir, before, NULL);
493 if (dest == NULL || do_rename(before_path, dest))
495 /* Advances iter */
496 if (!gtk_list_store_remove(list, &iter))
497 break; /* Last item; finished */
499 else
500 success = FALSE;
502 g_free(before_path);
503 g_free(before);
504 g_free(after);
507 fail:
508 g_free(slash_example);
509 if (names)
510 g_hash_table_destroy(names);
511 return success;
514 static void cell_edited(GtkCellRendererText *cell,
515 const gchar *path_string, const gchar *new_text,
516 GtkTreeModel *model)
518 GtkTreePath *path;
519 GtkTreeIter iter;
521 path = gtk_tree_path_new_from_string(path_string);
522 gtk_tree_model_get_iter(model, &iter, path);
523 gtk_tree_path_free(path);
525 gtk_list_store_set(GTK_LIST_STORE(model), &iter, 1, new_text, -1);
528 #ifdef UNIT_TESTS
529 static void test_subst(const char *string, const char *pattern, const char *with, const char *expected)
531 regex_t compiled;
532 GString *new;
534 g_print("Testing s/%s/%s\n", pattern, with);
536 if (regcomp(&compiled, pattern, REG_EXTENDED))
537 g_error("Failed to compiled '%s'", pattern);
539 new = subst(string, &compiled, with);
541 if (new == NULL)
543 g_return_if_fail(expected == NULL);
545 else
547 //g_print("Got: %s\n", new->str);
548 g_return_if_fail(expected != NULL);
549 g_return_if_fail(strcmp(new->str, expected) == 0);
550 g_string_free(new, TRUE);
553 regfree(&compiled);
556 void bulk_rename_tests()
558 test_subst("hello", "l", "L", "heLlo");
559 test_subst("hello", "h(.*)l", "\\1-\\1", "el-elo");
560 test_subst("hello", "(h)", "\\1", "hello");
561 test_subst("hello", "(h)", "\\\\1", "\\1ello");
562 test_subst("hello", "(h)", "\\\\\\1", "\\hello");
563 test_subst("hello", "(h)", "\\", "\\ello");
564 test_subst("hello", "(h)$", "\\", NULL);
565 test_subst("hello", "(.)(.)(.).*", "\\0-\\1-\\2-\\3-\\4-\\9-\\:", "hello-h-e-l---:");
567 #endif