-
Notifications
You must be signed in to change notification settings - Fork 0
/
lns
314 lines (248 loc) · 8.78 KB
/
lns
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
#!/usr/bin/perl
# desc{ a friendly program for making symbolic links }
$VERSION = '2.01'; # Time-stamp: "2008-08-19 19:26:35 AKDT [email protected]"
=head1 NAME
lns -- a friendly program for making symbolic links
=head1 SYNOPSIS
lns target-filespec symlink-filespec
or
lns symlink-filespec target-filespec
=head1 DESCRIPTION
It's easy to make mistakes when you're using F<ln -s> to make
symlinks. So use this program, F<lns>, instead -- it's basically F<ln
-s> plus lots of sanity-checking and DWIM ("do what I mean").
Notably, it doesn't care whether you say C<lns target symlink> or
C<lns symlink target>.
=head1 EXAMPLE USES
Here's a short example session containing attempts to use F<lns> to make
some symlinks:
% ls -l
-rw-r--r-- 1 sburke dongs 5235 Feb 29 20:52 stuff.html
% lns stuff.html index.html
Made index.html -> stuff.html
% ls -l
lrwxr-xr-x 1 sburke dongs 10 Feb 29 22:43 index.html -> stuff.html
-rw-r--r-- 1 sburke dongs 5235 Aug 19 22:43 stuff.html
% lns funk.txt fank.dat
But neither funk.txt nor fank.dat exist!
% lns index.html stuff.html
But both index.html and stuff.html already exist.
Maybe rm the symlink index.html (->stuff.html)?
% lns . foo
lns doesn't allow symlinking to or from "."
=head1 OPTIONS
Currently, the only command-line option is C<lns -v>, which prints the
lns version number and aborts.
=head1 SEE ALSO
The man page for F<ln>.
=head1 BUG REPORTS
If this program acts up, email me about it, at C<[email protected]>.
=head1 COPYRIGHT AND DISCLAIMER
Copyright (c) 2004 Sean M. Burke. All rights reserved.
This library is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
(See L<perlartistic> and L<perlgpl>.)
The program and its documentation are distributed in the hope that
they will be useful, but without any warranty; without even the
implied warranty of merchantability or fitness for a particular
purpose. But let me know if it gives you trouble, okay?
=head1 AUTHOR
Sean M. Burke, C<[email protected]>.
=head1 SCRIPT CATEGORIES
UNIX/System_administration
=head1 CHANGE LOG
=over
=item v2.01 2004-08-20
First CPAN release, after maybe four years of using it on my own and
passing it around to friends. All that's new in this version is the
documentation, and the "-v" option.
=back
=cut
require 5;
#===========================================================================
if( @ARGV and $ARGV[0] eq '-v' ) {
print "lns v$VERSION sburke\x40cpan.org\n";
exit;
} elsif( @ARGV != 2) {
die "Usage: lns symlink_to_make source_filespec (or vice versa)\n",
" See 'perldoc lns' for more information.\n",
}
use strict;
#--------------------------------------------------------------------------
sub DEBUG () {0}
my($from, $to) = @ARGV;
# $from is the spec of the link to make.
# $to is what it should point to.
die "Can't use empty-string as a filespec.\n"
unless length $from and length $to;
die "But source and target are the same ($from)!" if $from eq $to;
foreach my $x ($from, $to) {
if($x =~ s</+$><>s) { # kill trailing /'s
$x = '/' if $x eq '';
}
}
die "lns doesn't allow symlinking to or from \"..\"\n"
if $to eq '..' or $from eq '..';
die "lns doesn't allow symlinking to or from \".\"\n"
if $to eq '.' or $from eq '.';
# Technically, it'd be possible to link anything TO . or ..,
# but it's so icky I'll disallow it.
# Assert that $from doesn't exist and $to exists; and $to's not
# a symlink, nor '.' nor '..'
if(-e $from or -l $from) {
# Why not just "-e $from"? because "-e $from" is false if $from
# is a dangling symlink
#
if(-e $to or -l $to) {
# They both exist!
if(-l $from) {
if(-l $to) {
die "But both $from and $to already exist, and are both symlinks!\n";
} else {
die "But both $from and $to already exist.\nMaybe rm the symlink $from (->",
readlink($from), ")?\n";
}
} else {
if(-l $to) {
die "But both $from and $to already exist.\nMaybe rm the symlink $to (->",
readlink($to), ")?\n";
} else {
die "But both $from and $to already exist, and neither are symlinks.\n";
}
}
} else {
# One exists, the other doesn't, but they need switching.
($from, $to) = ($to, $from);
}
} else {
# $from doesn't exist
if(-e $to or -l $to) {
# One exists, the other doesn't, and they're each in the right place.
} else {
die "But neither $from nor $to exist!\n";
}
}
# If we're putting the symlink somewhere else, make sure
# the directory we want to put it in exists.
if($from =~ m<^(.*/)[^/]+$>) {
die
"But the directory $1 doesn't exist for the symlink $from to be put in!\n"
unless -e $1;
# Altho it may actually be a dangling symlink. Not our problem, really.
}
if($from =~ m</> and $to !~ m<^/>) {
# The $from is in another dir, and the $to is relative.
# We /expect/ the $to to be interpreted relative to the pwd.
# However, we'll need to re-relativize it for sake of symlinking,
# so we can have a pathspec to it that's relative to $from's base
# directory.
# If it turns out that interpreting original $to relative to
# $from's base dir gives us an existing file too, then scream,
# in case the user's mixed up as to which is meant.
# However, note that unless $to (relative to pwd) existed, we'd
# never have gotten this far!
# This is all a bit of a mess, and if I had it to do over again,
# I might just make this refuse to deal with $froms in other dirs
# unless $to is absolute. I don't know if that's detectable, tho,
# since all the "what exists / what doesn't" code, above, already
# assumes that relative things are relative to PWD.
my $f_dir = $from;
my $f_base;
if($f_dir =~ s</([^/]+)$><>) {
$f_base = $1;
$f_dir = '/' unless length $f_dir;
} else {
die "SNORT";
}
my $pwd = `pwd`;
chomp $pwd;
$pwd = abs2rel($pwd, '/');
my $f_dir_abs = rel2abs($f_dir, $pwd);
DEBUG and print "f_dir_abs: [$f_dir_abs] pwd: [$pwd]\n";
my $to_abs = rel2abs($to, $f_dir_abs);
my $to_alt_abs = rel2abs($to, $pwd);
my $to_alt_rel = abs2rel($to_alt_abs, $f_dir_abs);
if(-e $to_abs or -l $to_abs) {
die "Does \"$to\" refer to $to_alt_abs or $to_abs? Both exist.\n",
"Depending on which you mean, run one of these:\n",
" cd $f_dir; lns $f_base $to\n", # if rel to $f_dir_abs
" or: cd $f_dir; lns $f_base $to_alt_rel\n", # if rel to $pwd
;
} else {
# It's not really ambiguous -- the other reading doesn't refer
# to an existing file.
print "(From $from\'s perspective, \"$to\" is \"$to_alt_rel\")\n";
$to = $to_alt_rel;
}
}
# Now actually do it
if( symlink($to, $from) ) {
print "Made $from -> $to\n";
} else {
die "Couldn't make symlink from $from to $to: $!\n";
}
exit;
# "It isn't necessary to imagine the world ending in fire or ice -- there are
# two other possibilities: one is paperwork, and the other is nostalgia."
# -- Frank Zappa
#...........................................................................
#
# The subs below here are of my own devising. For real
# things, use File::PathConvert from CPAN.
sub rel2abs {
# a bit of a hack?
my($spec, $base) = @_;
$base = '' if $spec =~ m<^/>;
my @bits = grep length $_, split m</+>, "$base/$spec";
DEBUG and print "rel2abs stack: [@bits]\n";
_dirlist_proc(\@bits);
DEBUG and print " outstack: [@bits]\n";
return '/' unless @bits;
return join '/', '', @bits;
}
sub abs2rel {
my($spec, $base) = @_;
return $spec unless $spec =~ m<^/>s; # sanity?
die "Base <$base> isn't absolute" unless $base =~ m<^/>s;
return $spec if $base eq '/'; # more sanity
my @spec = grep length $_, split m</+>, $spec;
my @base = grep length $_, split m</+>, $base;
DEBUG and print "1- base [@base] spec [@spec]\n";
_dirlist_proc(\@base);
_dirlist_proc(\@spec);
# eat away common initial parts. Assumes no parts are ".."!
my $cut_out;
while(@base and @spec and $base[0] eq $spec[0]) {
shift @base; shift @spec;
++$cut_out;
}
return join '/', '', @spec unless $cut_out;
# They had nothing in common. Return an absolute ref, I guess.
# Otherwise cdup to common dir, then have spec bits to go down again.
unshift @spec, ('..') x scalar(@base);
DEBUG and print "2- base [@base] spec [@spec]\n";
return '.' unless @spec;
return join '/', @spec;
}
sub _dirlist_proc {
my $b = $_[0];
for(my $i = 0; $i < @$b;) {
if($b->[$i] eq '..') {
# CDUP
if($i == 0) {
shift @$b; # just nix myself and run
} else {
splice @$b, $i-1, 2;
--$i;
}
} elsif($b->[$i] eq '.') {
# IDEM
shift @$b; # just nix myself and run
} else {
# Normal path bit.
++$i;
}
}
}
#---------------------------------------------------------------------------
__END__