changelog
[moreutils.git] / vidir
blob656ce4feb1d4aff82adb122c4923c1d9d8f94e5e
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 foreach (@dir) {
124 next if /^(.*\/)?\.$/ || /^(.*\/)?\.\.$/;
125 $item{++$c}=$_;
126 print OUT "$c\t$_\n";
128 @dir=();
129 close OUT || die "$0: cannot write ".$tmp->filename.": $!\n";
131 my @editor="vi";
132 if (-x "/usr/bin/editor") {
133 @editor="/usr/bin/editor";
135 if (exists $ENV{EDITOR}) {
136 @editor=split(' ', $ENV{EDITOR});
138 if (exists $ENV{VISUAL}) {
139 @editor=split(' ', $ENV{VISUAL});
141 $ret=system(@editor, $tmp);
142 if ($ret != 0) {
143 die "@editor exited nonzero, aborting\n";
146 open (IN, $tmp->filename) || die "$0: cannot read ".$tmp->filename.": $!\n";
147 while (<IN>) {
148 chomp;
149 if (/^(\d+)\t{0,1}(.*)/) {
150 my $num=int($1);
151 my $name=$2;
152 if (! exists $item{$num}) {
153 die "$0: unknown item number $num\n";
155 elsif ($name ne $item{$num}) {
156 next unless length $name;
157 my $src=$item{$num};
158 my $dir=dirname($name);
160 if (! (-e $src || -l $src) ) {
161 print STDERR "$0: $src does not exist\n";
162 delete $item{$num};
163 next;
166 # deal with swaps
167 if (-e $name || -l $name) {
168 my $tmp=$name."~";
169 my $c=0;
170 while (-e $tmp || -l $tmp) {
171 $c++;
172 $tmp=$name."~$c";
174 if (! rename($name, $tmp)) {
175 print STDERR "$0: failed to rename $name to $tmp: $!\n";
176 $error=1;
178 elsif ($verbose) {
179 print "'$name' -> '$tmp'\n";
181 foreach my $item (keys %item) {
182 if ($item{$item} eq $name) {
183 $item{$item}=$tmp;
188 if ((! -d $dir) && (! make_path($dir, {
189 verbose => $verbose,
190 }))) {
191 print STDERR "$0: failed to create directory tree $dir: $!\n";
192 $error=1;
194 elsif (! rename($src, $name)) {
195 print STDERR "$0: failed to rename $src to $name: $!\n";
196 $error=1;
198 else {
199 if (-d $name) {
200 foreach (values %item) {
201 s,^\Q$src\E($|/),$name$1,;
204 if ($verbose) {
205 print "'$src' => '$name'\n";
209 delete $item{$num};
211 elsif (/^\s*$/) {
212 # skip empty line
214 else {
215 die "$0: unable to parse line \"$_\", aborting\n";
218 close IN || die "$0: cannot read ".$tmp->filename.": $!\n";
219 unlink($tmp.'~') if -e $tmp.'~';
221 sub rm {
222 my $file = shift;
224 if (-d $file && ! -l $file) {
225 return rmdir $file;
227 else {
228 return unlink $file;
232 foreach my $item (reverse sort values %item) {
233 if (! rm($item)) {
234 print STDERR "$0: failed to remove $item: $!\n";
235 $error=1;
237 if ($verbose) {
238 print "removed '$item'\n";
242 exit $error;