How to make rm not delete symbolic links? - bash

So, I was getting rid of the unwanted files and I deleted some folder. After some time I saw another folder which appears red in ls -la. I knew I deleted the actual folder and thus I was very sad and tried to run foremost to recover but bad luck. Can anyone give me some good aliases/function that deletes like rm -rf but if that file is linked to other files then it won't delete. Like if /usr/bin/a.py is linked to /root/a.py and I run rm -rf /usr/bin/a.py then /root/a.py will be deleted. How can I prevent that from an alias?

Joe W's answer is correct in saying rm typically does not delete the targets of symlinks, but to say that it "does not follow symlinks" is not quite accurate, at least on my system (GNU coreutils 8.25). And deleting files is a place where accuracy is pretty important! Let's take a look at how it behaves in a few situations.
If your symlink is to a file, rather than to a directory, there is no plausible way to accidentally delete the target using rm. You would have to do something very explicit like rm "$(readlink file)".
Symlinks to directories, however, get a bit dicey, as you saw when you accidentally deleted one. Here's a test case we can use:
$ mkdir test1
$ touch test1/foo.txt
$ ln -s test1 test2
$ ls -lR
.:
total 4
drwxrwxr-x 2 soren soren 4096 Jun 29 17:02 test1
lrwxrwxrwx 1 soren soren 5 Jun 29 17:02 test2 -> test1
./test1:
total 0
-rw-rw-r-- 1 soren soren 0 Jun 29 17:02 foo.txt
These are all safe:
rm test2 (deletes the symlink only)
rm -r test2 (deletes the symlink only)
rm -rf test2 (deletes the symlink only)
rm test2/ (rm: cannot remove 'test2/': Is a directory -- no action taken)
rm -rf *2 (or any other glob matching the symlink -- deletes the symlink only)
These are not safe:
rm -r test2/ (rm: cannot remove 'test2/': Not a directory -- but deletes the contents of the test1 directory)
rm -rf test2/ (deletes the contents of the directory, leaves the symlink, no error)
rm -rf test2/* (deletes the contents of the directory, leaves the symlink, no error)
The last unsafe case is probably obvious behavior, at least to someone well-acquainted with the shell, but the two before it are quite a bit more subtle and dangerous, especially since tab-completing the name of test2 will drop the trailing slash in for you!
It's interesting to note that test has similar behavior, considering a symlink to a directory with a trailing slash to be not a symlink but a directory, while a symlink without a trailing slash is both:
$ [ -L "test2" ] && echo "is link"
is link
$ [ -d "test2" ] && echo "is directory"
is directory
$ [ -L "test2/" ] && echo "is link"
$ [ -d "test2/" ] && echo "is directory"
is directory
Here's a previous treatment of "deleting a symlink to a directory without deleting the target," with a less thorough analysis of exactly what works and what doesn't but with a bunch of other useful information.
Unfortunately, I am not aware of any way to alias rm to prevent this mistake. I suppose you could write a function to parse the arguments to rm and warn you if any of them are symlinks that end with a trailing slash, something like this:
function rm {
for i in "$#"; do
if [[ $i =~ /$ ]] && [ -L "${i:0:-1}" ]; then
read -rp "Really delete symlink '$i' with trailing slash (y/n)? " result
[ "$result" != "y" ] && return
fi
done
command rm "$#"
}
Use at your own risk, though! It passes shellcheck and it worked when I tested it, but implementing a wrapper on top of something as fundamental and dangerous as rm gives me the heebie-jeebies.
Two other potentially useful switches you might include in your alias/function would be --one-file-system (at least skips files past a symlink or mount onto a different drive) and, if you don't already use it, -i or -I to prompt when doing something potentially dangerous.

I don't think that it is behaving the way you're indicating. rm doesn't follow symlinks
https://superuser.com/questions/520058/does-rm-r-follow-symbolic-links
https://superuser.com/questions/382314/does-rm-rf-follow-symbolic-links
The only way you could delete something is if you did something like rm -rf myFolder/* where myFolder is a linked directory.

Related

bash script to remove files matching those in another directory

I'm trying to create a script that retrieves files (including subfolders) from CVS and stores them into a temporary directory /tmp/projectdir/ (OK), then removes copies of those files from my project directory /home/projectdir/ (not OK) without touching any other files in the project directory or the folder structure itself.
I've been attempting two methods, but I'm running into problems with both. Here's my script so far:
#!/usr/bin/bash
cd /tmp/
echo "removing /tmp/projectdir/"
rm -rf /tmp/projectdir
# CVS login goes here, code redacted
# export files to /tmp/projectdir/dir_1/file_1 etc
cvs export -kv -r $1 projectdir
# method 1
for file in /tmp/projectdir/*
do
# check for zero-length string
if [-n "$file"]; then
echo "removing $file"
rm /home/projectdir/"$file"
fi
done
# method 2
find /tmp/projectdir/ -exec rm -i /home/projectdir/{} \;
Neither method works as intended, because I need some way of stripping /tmp/projectdir/ from the filename (to be replaced with /home/projectdir/) and to prevent them from executing rm /home/projectdir/dir_1 (i.e. the directory and not a specific file), but I'm not sure how to achieve this.
(In case anybody is wondering, the zero-length string bit was an attempt to avoid rm'ing the directory, before I realised /tmp/projectdir/ would also be a part of the string)
You can use:
cd /tmp/projectdir/
find . -type f -exec rm -i /home/projectdir/{} \;

Collapse nested directories in bash

Often after unzipping a file I end up with a directory containing nothing but another directory (e.g., mkdir foo; cd foo; tar xzf ~/bar.tgz may produce nothing but a bar directory in foo). I wanted to write a script to collapse that down to a single directory, but if there are dot files in the nested directory it complicates things a bit.
Here's a naive implementation:
mv -i $1/* $1/.* .
rmdir $1
The only problem here is that it'll also try to move . and .. and ask overwrite ./.? (y/n [n]). I can get around this by checking each file in turn:
IFS=$'\n'
for file in $1/* $1/.*; do
if [ "$file" != "$1/." ] && [ "$file" != "$1/.." ]; then
mv -i $file .
fi
done
rmdir $1
But this seems like an inelegant workaround. I tried a cleaner method using find:
for file in $(find $1); do
mv -i $file .
done
rmdir $1
But find $1 will also give $1 as a result, which gives an error of mv: bar and ./bar are identical.
While the second method seems to work, is there a better way to achieve this?
Turn on the dotglob shell option, which allows the your pattern to match files beginning with ..
shopt -s dotglob
mv -i "$1"/* .
rmdir "$1"
First, consider that many tar implementations provide a --strip-components option that allows you to strip off that first path. Not sure if there is a first path?
tar -tf yourball.tar | awk -F/ '!s[$1]++{print$1}'
will show you all the first-level contents. If there is only that one directory, then
tar --strip-components=1 -tf yourball.tar
will extract the contents of that directory in tar into the current directory.
So that's how you can avoid the problem altogether. But it's also a solution to your immediate problem. Having extracted the files already, so you have
foo/bar/stuff
foo/bar/.otherstuff
you can do
tar -cf- foo | tar --strip-components=2 -C final_destination -xf-
The --strip-components feature is not part of the POSIX specification for tar, but it is on both the common GNU and OSX/BSD implementations.

Truncate a directory in bash

Is there some elegant/simple way to delete the folder's contents in such a way there's no error output if it is empty?
The following command
$ rm -r $dir/*
doesn't work if the directory is empty, since in such a case, the wilcard * is not expanded and you get an error saying that rm cannot find file *.
Of course, the standard way is check if it is empty (with ls $dir | wc -w or find $dir -link 2 or any other related command), and deleting its contents otherwise.
Is there an alternative way not to check folder contents and only "truncate" the directory instead?
Bash
Simply,
$ rm -rf dir/*
(By default I believe) Bash doesn't complain about not finding anything with the glob. It just passes your literal glob through to your command:
$ echo dir/*
dir/*
When rm doesn't find a filename that has the literal glob character, it complains about not finding the file it's been asked to delete:
$ rm "dir/*"
rm: cannot remove ‘dir/*’: No such file or directory
$ echo $?
1
But if you force it, it won't complain:
$ rm -f "dir/*"
$ echo $?
0
I don't know if that refrain-from-complain is POSIX.
Do note, however, that if you don't have the shell option "dotglob" set, you'll miss files that start with a dot, AKA "hidden" files.
Generally
Zsh doesn't pass the literal glob through by default. You have to ask for it with "set -o nonomatch".
$ echo dir/*
zsh: no matches found: dir/*
$ echo $?
1
$ set -o nonomatch
$ echo dir/*
dir/*
For compatibility, I wouldn't use the above modern-Bash-specific "rm -rf dir/*", but would use the more general, widely-compatible solution:
$ find dir -mindepth 1 -delete
Find all files in "dir" at a minimum depth of 1 ("dir" itself is at depth 0), and delete them.
You can use rm -rf:
rm -rf "$dir"/*
As per man bash:
-f, --force
ignore nonexistent files and arguments, never prompt
rm -rf dir/*
does not delete hidden files which name starts with dot.
This is quite weird, when bash glob the *, it does not include .* files.
mkdir -p dir
touch dir/.a
rm -fr dir/*
ls dir/.a && echo I am not deleted
output is
dir/.a
I am not deleted
Besides, the rm -fr dir/* has another disadvantage: when there are too many files in the dir, the rm command will get too many arguments and results in error too many arguments. Also, it is very slow in that case.
Seems that the most reliable and fastest way is
find dir -mindepth 1 -delete

what is the diff between rm -rf $TIGER/${LION}/${RABBIT}/* and rm -rf $TIGER/${LION}/${RABBIT}?

I need to understand the diff between rm -rf $TIGER/${LION}/${RABBIT}/* and rm -rf $TIGER/${LION}/${RABBIT} so that putting this in script won't produce the disaster making it to delete everything it can from root in case that the variables are not set. what is the safe way to use rm -rf in csh/ksh?
Thanks for help !!
Either of those commands would create a disaster if the variables were all unset; they differ only in whether they delete the directory itself, or its non-hidden contents.
If you want to be safe against deleting recursively from the root directory, explicitly test for that case and cancel:
[[ $TIGER && $LION && $RABBIT ]] || {
echo "TIGER, LION and RABBIT must all be set; script exiting"
exit 1
}
rm -rf ...
This recursively deletes all non-hidden files inside the ${RABBIT} directory - ${RABBIT} directory is not deleted:
rm -rf $TIGER/${LION}/${RABBIT}/*
Note hidden files (aka dot files) have filenames beginning with .. These are not matched with typical * expansion unless shell dotglob option is set.
So to delete all files (including hidden files) you could use shopt thus:
shopt -s dotglob # turns shell option dotglob ON
rm -rf $TIGER/${LION}/${RABBIT}/* # Now deletes all (including hidden) files
shopt -u dotglob # FYI - unsets or turns dotglob OFF
This recursively deletes everything including the ${RABBIT} directory.
rm -rf $TIGER/${LION}/${RABBIT}
putting /* at the end will delete contents inside that directory
while only "/" will delete the directory itself as well as contents inside it.

What is causing "Directory not empty" errors?

I've written a script that is mostly working for me. However, the two rmdir commands at the end are sometimes returning Directory not empty errors. How do I beat this? I tried adding r and then -rf which returned Illegal option errors.
I'm a rookie, and I'd be grateful for any help. Here is the full shell script, which, again, is mostly working beautifully:
if [[ -z "${1}" ]]; then
die "FolderName Required"
fi
newDirName="DirectoryName"
newBaseDir="/Users/JSG/Desktop/DataFarm/$1/"
/bin/mkdir -p $newBaseDir/{ProtectedOrig,Data}
echo -n "---Data Folder Setup
---Data Introduction
---Data Audit/Manipulation
---Data Queries" > $newBaseDir/Data/$1_DataJournal.txt
ditto NewData/ NewDataCopy
fab deploy_data_to_s3:data=*
mv NewData/ $newBaseDir/ProtectedOrig/NewData
mv NewDataCopy/ $newBaseDir/Data/NewDataCopy
mv $newBaseDir/Data/NewDataCopy/* $newBaseDir/Data/
rmdir $newBaseDir/Data/NewDataCopy
mv $newBaseDir/ProtectedOrig/NewData/* $newBaseDir/ProtectedOrig/
rmdir $newBaseDir/ProtectedOrig/NewData
chflags -R uchg $newBaseDir/ProtectedOrig/
mkdir NewData
What am I missing? And thanks in advance!
For the rmdir command, you need to add the --ignore-fail-on-non-empty flag so it deletes the directory even if files are in there like so:
rmdir --ignore-fail-on-non-empty $newBaseDir/Data/NewDataCopy
You could also just use rm -r too:
rm -r $newBaseDir/Data/NewDataCopy
From the Wikipedia Entry:
rmdir will not remove a directory if it is not empty in UNIX. The correct way to remove a directory and all its contents recursively is with the rm command.
Check for any files in the directory that start with .. I note you're moving *, but if there's a file called, for example, .hello, then * will not match this file and as a result the directory will not be empty when you come to do an rmdir.

Resources