changelog
[moreutils.git] / vidir
blobea9b5c9f16daa0b48a00732aa0d8a32684f720af
1 #!/usr/bin/perl
3 =head1 NAME
5 vidir - edit directories and filenames
7 =head1 SYNOPSIS
9 B<vidir> [B<--verbose>] [I<directory>|I<file>|B<->]...
11 =head1 DESCRIPTION
13 B<vidir> allows editing of directories and filenames in a text editor. If no
14 I<directory> is specified, the filenames of the current directory are edited.
16 When editing a directory, each item in the directory will appear on its own
17 numbered line. These numbers are how vidir keeps track of what items are
18 changed. Delete lines to remove files from the directory, or
19 edit filenames to rename files. You can also switch pairs of numbers to
20 swap filenames.
22 Filenames to be edited may be given any combination of I<directory>s (which
23 will be expanded to the non-recursive list of all files within I<directory>),
24 I<file>s, or I<->. If the latter is specified, B<vidir> reads a list of
25 filenames from stdin and displays those for editing.
27 =head1 OPTIONS
29 =over 4
31 =item -v, --verbose
33 Verbosely display the actions taken by the program.
35 =back
37 =head1 EXAMPLES
39 =over 4
41 =item vidir
43 =item vidir *.jpeg
45 Typical uses.
47 =item find | vidir -
49 Edit subdirectory contents too. To delete subdirectories,
50 delete all their contents and the subdirectory itself in the editor.
52 =item find -type f | vidir -
54 Edit all files under the current directory and subdirectories.
56 =back
58 =head1 ENVIRONMENT VARIABLES
60 =over 4
62 =item EDITOR
64 Editor to use.
66 =item VISUAL
68 Also supported to determine what editor to use.
70 =back
72 =head1 AUTHOR
74 Copyright 2006 by Joey Hess <id@joeyh.name>
76 Licensed under the GNU GPL.
78 =cut
80 use File::Basename;
81 use File::Path qw(make_path);
82 use File::Spec;
83 use File::Temp;
84 use Getopt::Long;
86 my $error=0;
88 my $verbose=0;
89 if (! GetOptions("verbose|v" => \$verbose)) {
90 die "Usage: $0 [--verbose] [directory|file|-]\n";
93 my @dir;
94 if (! @ARGV) {
95 push @ARGV, "."
97 foreach my $item (@ARGV) {
98 if ($item eq "-") {
99 push @dir, map { chomp; $_ } <STDIN>;
100 close STDIN;
101 open(STDIN, "/dev/tty") || die "reopen: $!\n";
103 elsif (-d $item) {
104 $item =~ s{/?$}{/};
105 opendir(DIR, $item) || die "$0: cannot read $item: $!\n";
106 push @dir, map { "$item$_" } sort readdir(DIR);
107 closedir DIR;
109 else {
110 push @dir, $item;
114 if (grep(/[[:cntrl:]]/, @dir)) {
115 die "$0: control characters in filenames are not supported\n";
118 my $tmp=File::Temp->new(TEMPLATE => "dirXXXXX", DIR => File::Spec->tmpdir);
119 open (OUT, ">".$tmp->filename) || die "$0: cannot create ".$tmp->filename.": $!\n";
121 my %item;
122 my $c=0;
123 my $l = $#dir =~ tr/0-9//;
124 foreach (@dir) {
125 next if /^(.*\/)?\.$/ || /^(.*\/)?\.\.$/;
126 $item{++$c}=$_;
127 printf OUT "%0*d\t%s\n", $l, $c, $_;
129 @dir=();
130 close OUT || die "$0: cannot write ".$tmp->filename.": $!\n";
132 my @editor="vi";
133 if (-x "/usr/bin/editor") {
134 @editor="/usr/bin/editor";
136 if (exists $ENV{EDITOR}) {
137 @editor=split(' ', $ENV{EDITOR});
139 if (exists $ENV{VISUAL}) {
140 @editor=split(' ', $ENV{VISUAL});
142 $ret=system(@editor, $tmp);
143 if ($ret != 0) {
144 die "@editor exited nonzero, aborting\n";
147 open (IN, $tmp->filename) || die "$0: cannot read ".$tmp->filename.": $!\n";
148 while (<IN>) {
149 chomp;
150 if (/^(\d+)\t{0,1}(.*)/) {
151 my $num=int($1);
152 my $name=$2;
153 if (! exists $item{$num}) {
154 die "$0: unknown item number $num\n";
156 elsif ($name ne $item{$num}) {
157 next unless length $name;
158 my $src=$item{$num};
159 my $dir=dirname($name);
161 if (! (-e $src || -l $src) ) {
162 print STDERR "$0: $src does not exist\n";
163 delete $item{$num};
164 next;
167 # deal with swaps
168 if (-e $name || -l $name) {
169 my $tmp=$name."~";
170 my $c=0;
171 while (-e $tmp || -l $tmp) {
172 $c++;
173 $tmp=$name."~$c";
175 if (! rename($name, $tmp)) {
176 print STDERR "$0: failed to rename $name to $tmp: $!\n";
177 $error=1;
179 elsif ($verbose) {
180 print "'$name' -> '$tmp'\n";
182 foreach my $item (keys %item) {
183 if ($item{$item} eq $name) {
184 $item{$item}=$tmp;
189 if ((! -d $dir) && (! make_path($dir, {
190 verbose => $verbose,
191 }))) {
192 print STDERR "$0: failed to create directory tree $dir: $!\n";
193 $error=1;
195 elsif (! rename($src, $name)) {
196 print STDERR "$0: failed to rename $src to $name: $!\n";
197 $error=1;
199 else {
200 if (-d $name) {
201 foreach (values %item) {
202 s,^\Q$src\E($|/),$name$1,;
205 if ($verbose) {
206 print "'$src' => '$name'\n";
210 delete $item{$num};
212 elsif (/^\s*$/) {
213 # skip empty line
215 else {
216 die "$0: unable to parse line \"$_\", aborting\n";
219 close IN || die "$0: cannot read ".$tmp->filename.": $!\n";
220 unlink($tmp.'~') if -e $tmp.'~';
222 sub rm {
223 my $file = shift;
225 if (-d $file && ! -l $file) {
226 return rmdir $file;
228 else {
229 return unlink $file;
233 foreach my $item (reverse sort values %item) {
234 if (! rm($item)) {
235 print STDERR "$0: failed to remove $item: $!\n";
236 $error=1;
238 if ($verbose) {
239 print "removed '$item'\n";
243 exit $error;