2

I found a lot of questions on how to delete symlinks only, or delete symlinks based on their contents, but no one has asked how to do what I want to do.

Take the following example

file-l -> ../file
file-l2 -> ../file

I want to delete file, file-l, and file-l2 in one fell swoop, without knowing the locations of the other files, preferably by specifying file as an argument. (imagine that I have 100+ links to the same file in random places all over the filesystem). Primarily, I would prefer a shell script or built-in program, but external programs are also okay.

After unlink and rm failed to provide the desired functionality, I initially had the idea to use inodes or stat or something, but...

$ stat test
  File: ‘test’
  Size: 0           Blocks: 0          IO Block: 4096   regular empty file
Device: 811h/2065d  Inode: 27001032    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/  braden)   Gid: ( 1000/  braden)
Access: 2015-08-30 21:30:35.578786598 -0600
Modify: 2015-08-30 21:30:35.578786598 -0600
Change: 2015-08-30 21:30:35.578786598 -0600
 Birth: -
$ stat test-l
  File: ‘test-l’ -> ‘test’
  Size: 4           Blocks: 0          IO Block: 4096   symbolic link
Device: 811h/2065d  Inode: 27001033    Links: 1
Access: (0777/lrwxrwxrwx)  Uid: ( 1000/  braden)   Gid: ( 1000/  braden)
Access: 2015-08-30 21:30:42.074786598 -0600
Modify: 2015-08-30 21:30:41.426786598 -0600
Change: 2015-08-30 21:30:41.426786598 -0600
 Birth: -

Not only does the symlink have a unique inode (that's a given) but there doesn't seem to be any information aside from the file name that I could use to conclude that "yes, this is a symlink to the file I want". If I use the filename, though, then I have to deal with regex-hell. This is something I would personally like to avoid since, in practice, the file names contain characters like space, meta-characters (?![]{}.,), and unicode characters.

I would also like to avoid things like awk, perl, crazy hacks that exploit bugs/design-flaws in bash 4.2.XX, and other weird stuff that probably wouldn't work properly (if at all) on a fresh install of, e.g, Arch Linux or CentOS. I should basically be able to run said command/script out-of-the-box on a system that lacks awk, sed, perl, ruby, and python, and runs bash-4.1 as its shell.

If that isn't possible, then I would at least prefer the script/command use/be ubiquitous, like vi, cat, ls, etc.

The bare minimum of my requirements is that the solution:

  • can delete a file and all of its symlinks with only the original file as an argument
  • can do so without knowing the names or locations of said symlinks
  • can do so on symlinks of varying names and varying locations
  • works in a fresh, minimal install of Arch Linux (or similar minimal system)
  • is not a code-golf
  • regexes reasonable and easy-to-understand

This is how I try to design my own scripts, so I consider it reasonable. But I nonetheless hope it isn't too demanding.

Thanks.


Edit: I feel like I'm obligated to show my work. Just requesting a script makes me feel like a jerk. So, this is what I have:

get_lnk(){
    gl_file="$1"
    gl_orig_a=( $(ls -l "$gl_file") )
    gl_orig="${gl_orig_a[-1]}"
}
for i in *; do 
    get_lnk "$i"
    echo "file: $gl_file"
    echo "links to: $gl_orig"
    echo
done

# output:

file: file-l
links to: ../file

file: file-l2
links to: ../file

This prints the name of the symlink and the name of the file it points to. This is actually the opposite of what I'm trying to do, but is reasonably useful (when all the links are in the same place).

  • I'm not trying to make a thread like those that ask for opinions, etc (and then get quickly closed for being unproductive). I'm more aiming for "how to do [thing] without getting too complicated" – Braden Best Aug 31 '15 at 3:58
  • 2
  • While at it - what about multiple links? I mean, if you have link1 -> link2 and link2 -> file? I think that @Cyrus link will not find them all, but I'm not sure. – Rmano Aug 31 '15 at 8:03
  • 1
    @cyrus hmm, didn't know find had an -L option. I only know about '-type l'. Excellent solution! I suggest you post this as a real answer – Braden Best Aug 31 '15 at 13:13
  • @Rmano interesting observation. Though if the -L version works on symlinks as well, you could feasibly make a loop to go thorough each result and recursively find all of their links – Braden Best Aug 31 '15 at 13:15
2

Let's say you have a file named file1 in ~/tmp, and let's say that you want to recursively remove all the symlinks to file1 in ~/tmp;

Using find:

find ~/tmp -lname ~/tmp/file1 -exec rm {} +
  • ~/tmp: specifies the path to the directory hierarchy in which to search;
  • -lname: searches only for symlinks linking to ~/tmp/file1;
  • -exec rm {} +: removes all the results;

Sample output on a test directory hierarchy:

user@debian ~/tmp % tree
.
├── 1
│   ├── 2
│   │   ├── 3
│   │   │   ├── link3-1 -> /home/user/tmp/file1
│   │   │   └── link3-2 -> /home/user/tmp/file2
│   │   ├── link2-1 -> /home/user/tmp/file1
│   │   └── link2-2 -> /home/user/tmp/file2
│   ├── link1-1 -> /home/user/tmp/file1
│   └── link1-2 -> /home/user/tmp/file2
├── file1
└── file2

3 directories, 8 files
user@debian ~/tmp % find . -lname ~/tmp/file1 -exec rm {} +
user@debian ~/tmp % tree
.
├── 1
│   ├── 2
│   │   ├── 3
│   │   │   └── link3-2 -> /home/user/tmp/file2
│   │   └── link2-2 -> /home/user/tmp/file2
│   └── link1-2 -> /home/user/tmp/file2
├── file1
└── file2

3 directories, 5 files
| improve this answer | |
  • Right on the mark. I personally avoid -exec because it's unintuitive as hell, and just wrap the find in a loop to make things simpler. However, would you care to explain the {} +? -exec rm is reasonable enough, and I suppose {} could be shorthand for "this", but the +? – Braden Best Sep 10 '15 at 7:27
  • @B1KMusic I prefer -exec for the simple reason that it won't break on edge cases such as filenames containing newlines, however reasonably enough that's, again, an edge case. Not at all, basically in the -exec switch you always reference a filename / a list of filenames using {}: if you choose to use {} followed by \;, {} will be replaced with a single filename and the command will run multiple times each time on a different filename; if you choose to use {} followed by +, {} will be replaced with the whole list of filenames and the command will run once for all the filenames – kos Sep 10 '15 at 7:51
  • @B1KMusic I.e. {} followed by + will be considered as a list of arguments; mind that when using this method everything after {} until + is ignored, so sometimes this method won't fit the command you need to run; also using this method might result in the command using a list of filenames whose length exceedes the kernel's ARG_MAX threshold, and consequently in an error; if that's the case however you might just run find ~/tmp -lname ~/tmp/file1 -exec rm {} \; in place of find ~/tmp -lname ~/tmp/file1 -exec rm {} + – kos Sep 10 '15 at 7:59
  • Thanks for the explanation, very thorough. Just out of curiosity, say I set IFS=' ', would this fix that newline edge case assuming none of the filenames have any spaces in them? What about simply surrounding $file with quotation marks? – Braden Best Sep 10 '15 at 14:39
  • @B1KMusic IFS=' ' would make bash split fields on spaces, so no, also if you're talking about a while read loop read itself plays a role, and setting $IFS on its own is not useful; the situation is further complicated if you're talking about a for loop; I don't think there's a way to make it work with a for loop at all. I don't know which $file you're talking about, but assuming a $file reference in a for / while loop that should be quoted regardless, otherwise commands would split on whitespaces. – kos Sep 10 '15 at 15:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.