Recursively putting files in a Recycle Bin in Unix - bash

I've written script to temporarily delete files in Unix and then save the file path so they can be restored. I have 2 functions, one to delete files, and one to delete a directory that also recursively deletes files. I have the appropriate commands to recognize the directory name and path as well as checking to see if files remain in the directory and then deleting them. At first I was getting an infinite loop but I made some changes and now, it's saying there is no such file or directory when I try to delete a directory. It still runs through the code but it doesn't delete any files. Can anyone figure this out?
#!/bin/bash
#checks to see if deleted folder exists. If it doesn't, it is created.
if [ ! -d ~/deleted ];
then
mkdir ~/deleted
fi
if [ ! -f ~/.restore.info ] ;
then
touch ~/.restore.info
fi
function recur_delete {
dir=$1 #this will indicate the directory name only
dirpath=$(dirname $dir) #gets the directory path
if [ "$( ls -A /$dirpath/$dir)" ]; #determines if the directory contains files.
then
filename=$(find dirpath/dir -type f -printf "%f\n" | head -1)
delete_file $filename #filename is found and sent to delete_file function to be deleted.
recur_delete $dir #function is called again to see if more files are present.
else
echo Directory is empty, the directory will be deleted
echo $dirpath/$dir >> ~/.restore.info
rmdir ${dirpath/$dir}
fi
}
function delete_file {
inode=$(stat -c%i $filename) #grabs inode # for the chosen filename.
filename=$1 #reinitializes the variable filename as the first argument
pwd=$(readlink -e $filename) #This gets the entire path for the chosen file
if $interactive
then
if [ $verbose = true ];
then
read -p "Are you SURE you want to delete $filename ????" i_input
if [ $i_input == "y" ] || [ $i_input == "Y" ];
then
mv $filename ~/deleted/${filename}_$inode
echo ${filename}_$inode:$pwd >> ~/.restore.info
echo $filename has been deleted. Congrats.
else
echo Nothing has been done, the file or files remain.
fi
else
read -p "Are you SURE you want to delete $filename ????" i_input
if [ $i_input == "y" ] || [ $i_input == "Y" ];
then
mv $filename ~/deleted/${filename}_$inode
echo ${filename}_$inode:$pwd >> ~/.restore.info
else
echo Aborted
fi
fi
elif $verbose
then
mv $filename ~/deleted/${filename}_$inode
echo ${filename}_$inode:$pwd >> ~/.restore.info
echo $filename has been deleted. Congrats.
else
mv $filename ~/deleted/${filename}_$inode
echo ${filename}_$inode:$pwd >> ~/.restore.info
echo Executed
fi
}
interactive=false
verbose=false
recursive=false
while getopts ivr OPTION
do
case $OPTION in
i) interactive=true;;
v) verbose=true;;
r) recursive=true;;
esac
done
shift $[OPTIND-1]
for i in $*
do
filename=$i
basefile=$(basename $i)
if [ "$i" == "" ];
then
echo No file provided
elif [ -d $filename ];
then
if [ $recursive = true ];
then
recur_delete $filename
else
echo This is a directory, please provide a file name.
fi
elif [ ! -f $filename ];
then
echo File does not exist
elif [ "$basefile" == "safe_rm" ];
then
echo Attempting to delete safe_rm - operation aborted!!!!
#This is the line that takes the filename to be deleted and modifies the
#experience based on what the user wants.
else
delete_file $filename
fi
done

This seems super-complicated. How about building something around
$ mkdir ~/.trashbin
$ mv /absolute/path/to/dir/or/file ~/.trashbin
for temp delete and then
$ mv ~/.trashbin/absolute/path/to/dir /absolute/path/to/dir
for restore?
You need one little fragment of code to get the old absolute path with the ~/.trashbin deleted, but that's simple.
Update
Aaah, it's the silly professor problem. I resemble that.
Okay, here's the deal: in any recursion, there has to be something that "gets smaller" in some sense with each call, and finally gets to the point where you stop recurring. In your case, that should be the results of ls -A. Step away from the code and examine what you really get from ls -A on an empty directory, and whether that evaluates to 0 or non-zero in that if. Hint: I bet it never does.

Related

UNIX How do delete my unnecessary/excessive lines in my output?

My script is a mimic of the rm command, long story short. Can anyone point out the errors/unnecessary lines I have in my remove script that causes my output to produce excessive/irrelevant lines? The code works as intended but it produces all these unnecessary/excessive/duplicate lines. Output below is what looks like when I try to remove 2 files in the same line and do some other simple commands. Thank you in advance. I appreciate any help.
input: sh remove file2 file4
output:
Executed
Executed
cannot remove file4: no such file or directory
stat: cannot stat 'file4': No such file or directory
mv: cannot stat 'file4': No such file or directory
Executed
mv: cannot stat 'file4': No such file or directory
File moved to recycle bin
#/bin/bash
function directory(){
if [ ! -d ~/deleted ]
then
mkdir ~/deleted
fi
if [ ! -f ~/.restore.info ]
then
touch ~/.restore.info
fi
}
function movefile(){
mv $1 ~/deleted/$1
echo "file moved to recycle bin"
}
function error_conditions(){
#prints error messages and checks if file is in project directory
if [ ! -f ~/project ]
then
echo "cannot remove $filename: no such file or directory"
elif [ -d ~/project ]
then
echo "cannot remove $filename: is a directory"
else
echo "missing operand"
fi
}
function delete_file(){
#gets inode for filename
inode=$(stat -c%i $filename)
filename=$1
#pwd=$(readlink -e$filename)
if $interactive
then
if [ $verbose = true ]; then
read -p "Are you sure you want to delete $filename?" i_input
if [ $i_input == "y" ] || [ $i_input == "Y" ]
then
mv $filename ~/delete/${filename}_inode
echo ${filename}_$inode:$pwd>>~/.restore.info
echo "$filename has been deleted"
else
echo "Nothing has been deleted"
fi
else
read -p "Are you sure you want to delete $filename?" i_input
if [ $i_input == "y" ] || [ $i_input == "Y" ];
then
mv $filename ~/deleted/${filename}_$inode
echo ${filename}_$inode:$pwd>>~/.restore.info
else
echo Aborted
fi
fi
elif $verbose
then
mv $filename ~/deleted/${filename}_inode
echo ${filename}_$inode:$inode:pwd>>~/.restore.info
echo "$filename has been deleted."
else
mv $filename ~/deleted/${filename}_$inode
echo ${filename}_$inode:$pwd>>~/.restore.info
echo Executed
fi
}
interactive=false
verbose=false
while getopts iv option
do
case $option in
i) interactive=true;;
v) verbose=true;;
esac
done
shift $[OPTIND-1]
for i in $*
do
filename=$i
baseline=$(basename $i)
if [ "$i" == "" ];
then
echo "No filename provided"
elif [ -d $filename ];
then
if [ ! $recursive = true ];
then
echo "Directory name provided, please provide a file"
fi
elif [ ! -f $filename ];
then
echo "File does not exist"
elif [ "$basefule" == "safe_rm" ]
then
echo "Attempting to delete safe_rm"
else
delete_file $filename
fi
done
#################################M A I N###############################
directory
error_conditions $*
delete_file $*
movefile $*
Please indent properly.
for i in $* should be for i in "$#" or simply for i.
In general, variables should be quoted
(e.g., "$1", "$i",
"$filename",
"$verbose", etc.)
$[expression] is obsolete. 
Use $((expression)).
Your main loop calls
delete_file $filename
(line 100).
Your delete_file function sets
filename=$1
(line 35),
which is somewhat redundant and therefore confusing.
You set baseline but never use it. 
You test (i.e., reference) $basefule without ever setting it. 
Are these meant to be the same variable?
The code says
if [ ! -f ~/project ]
then
echo "cannot remove $filename: no such file or directory"
︙
This is a very misleading message.
You have a big comment that says “M A I N”,
but the “main” code begins about 33 lines earlier.
The code doesfor i in $*
do
filename=$i # This is an example of terrible indenting.
︙
delete_file $filename
︙
donebut then, five lines later,delete_file $*
so you’re processing the files twice. 
So, even if delete_file succeeds the first time you call it,
the file will be gone when you call it a second time.
And, if you want to call a function (e.g., delete_file)
with all the arguments to the script,
you should use "$#" rather than $*.
And, if you’re going to call delete_file with a list of filenames,
then delete_file needs to iterate (loop) over those arguments. 
Your delete_file function only looks at $1.

UNIX how to make my script delete multiple files and wildcards?

I was given the task of making a remove script that imitates the rm command. As you know, the rm command deletes all files if you were to type something like rm file1 file2. Using this example, my script would only delete file2. Can anyone help me on how to make it so my remove script would delete all files listed? My script is below. I apologise if its a little messy, I am new to coding.
#!/bin/bash
function directory(){
#Checks if deleted directory & .restore.info file exists
#If they don't exist, it creates them
if [ ! -d ~/deleted ]
then
mkdir ~/deleted
fi
if [ ! -f ~/.restore.info ]
then
touch ~/.restore.info
fi
}
function movefile(){
#not currently using
mv "$1" ~/deleted/$1
echo "file moved to recycle bin"
}
function error_conditions(){
#not currently using
#Prints error messages and checks if file is in project directory
if [ ! -f ~/project ]
then
echo "cannot remove $filename: no such file or directory"
elif [ -d ~/project ]
then
echo "cannot remove $filename: is a directory"
else
echo "missing operand"
fi
}
function delete_file(){
#Gets inode for filename
#Takes user input and puts file wherever based on user input
inode=$(stat -c%i "$filename")
pwd=$(readlink -e $filename)
if "$interactive"
then
if [ "$verbose" = true ]; then
read -p "Are you sure you want to delete $filename? " user_input
if [ $user_input == "y" ] || [ $user_input == "Y" ] || [ $user_input == "yes" ] || [ $user_input == "Yes" ];
then
mv $filename ~/deleted/${filename}_$inode
#moves deleted file to deleted directory (with inode at end)
echo ${filename}_$inode:$pwd>>~/.restore.info
#stores info of removed file in .restore.info (with path)
echo "removed '$filename'"
else
echo "Nothing has been deleted"
fi
else
read -p "Are you sure you want to delete $filename? " user_input
if [ $user_input == "y" ] || [ $user_input == "Y" ] || [ $user_input == "yes" ] || [ $user_input == "Yes"];
then
mv "$filename" ~/deleted/${filename}_$inode
echo ${filename}_$inode:$pwd>>~/.restore.info
else
echo "Aborted"
fi
fi
elif "$verbose"
then
mv "$filename" ~/deleted/${filename}_$inode
echo ${filename}_$inode:$inode:pwd>>~/.restore.info
echo "removed '$filename'"
else
mv "$filename" ~/deleted/${filename}_$inode
echo ${filename}_$inode:$pwd>>~/.restore.info
echo "Executed"
fi
}
#Setting all flags to false
interactive=false
verbose=false
recursive=false
while getopts :ivr optionvar
do
case "$optionvar" in
i) interactive=true;;
v) verbose=true;;
r) recursive=true;;
esac
done
shift $((OPTIND-1)) #process arguments.
#doing error commands with help of recursive
for i in $*
do
filename=$i
basefile=$(basename $i)
if [ "$filename" == " " ];
then
echo "No filename provcided"
elif [ -d $filename ];
then
if [ ! $recursive = true ];
then
echo "Directory name provided, please provide a file"
fi
elif [ ! -f $filename ];
then
echo "File does not exist"
# elif [ "$basefile" == "safe_rm" ]
# then
# echo "Attempting to delete safe_rm"
fi
done
#################################M A I N###############################
directory
delete_file $*
#error_conditions $* #- this gives me duplicate output lines
#movefile "$#" - this gives me an unnecessary "mv: cannot stat" output line
I'm not going to do a detailed code review of your whole script, but here are a few notes.
You are looping over the arguments in the main part of your script, but then you're calling the delete function with multiple arguments. That function has no looping in it. Move the loop from main() to delete_files() (and note that I pluralized its name for clarity).
And speaking of main(), you might as well encapsulate that code (option processing, function dispatch, etc.) in a function of that name, then at the bottom of your script have a line that calls it: main "$#"
Don't use $* unless you need what it does and understand its use - instead use "$#" almost always and always quote it (with very rare exceptions)
Use indentation consistently
If your script doesn't need to be portable to shells other than Bash, then use Bash-specific features such as [[ ]] instead of [ ]
You're using both methods of naming a function at the same time (function f()). Use one or the other - parens are preferred over using function - so f () { ...; }
Use more quotes, some examples:
pwd=$(readlink -e "$filename")
mv "$filename" ~/deleted/"${filename}_$inode"
echo "${filename}_$inode:$pwd" >> ~/.restore.info
But I don't recommend using tilde (~) in scripts - use $HOME instead. And if you need to look up a user's home directory, use getent instead of other methods.

Check if a directory contains another directory

How do I check if a given directory contains another directory in shell. I want to pass 2 full path directories. (I know this is stupid, but just for learning purposes). Then I want to see if any one of those 2 paths is contained in the other one.
parent=$1
child=$2
if [ -d $child ]; then
echo "YES"
else
echo "NO"
fi
this however makes no use of the parent directory. Only checks if the child exists.
You can use find to see if one name is contained within another:
result=$(find "$parent" -type d -name "$child")
if [[ -n $result ]]
then echo YES
else echo NO
fi
Create a file (ex: dircontains.sh) with this code:
#!/bin/bash
function dircontains_syntax {
local msg=$1
echo "${msg}" >&2
echo "syntax: dircontains <parent> <file>" >&2
return 1
}
function dircontains {
local result=1
local parent=""
local parent_pwd=""
local child=""
local child_dir=""
local child_pwd=""
local curdir="$(pwd)"
local v_aux=""
# parameters checking
if [ $# -ne 2 ]; then
dircontains_syntax "exactly 2 parameters required"
return 2
fi
parent="${1}"
child="${2}"
# exchange to absolute path
parent="$(readlink -f "${parent}")"
child="$(readlink -f "${child}")"
dir_child="${child}"
# direcory checking
if [ ! -d "${parent}" ]; then
dircontains_syntax "parent dir ${parent} not a directory or doesn't exist"
return 2
elif [ ! -e "${child}" ]; then
dircontains_syntax "file ${child} not found"
return 2
elif [ ! -d "${child}" ]; then
dir_child=`dirname "${child}"`
fi
# get directories from $(pwd)
cd "${parent}"
parent_pwd="$(pwd)"
cd "${curdir}" # to avoid errors due relative paths
cd "${dir_child}"
child_pwd="$(pwd)"
# checking if is parent
[ "${child_pwd:0:${#parent_pwd}}" = "${parent_pwd}" ] && result=0
# return to current directory
cd "${curdir}"
return $result
}
Then run these commands
. dircontains.sh
dircontains path/to/dir/parent any/file/to/test
# the result is in $? var
# $1=0, <file> is in <dir_parent>
# $1=1, <file> is not in <dir_parent>
# $1=2, error
Obs:
- Tested only in ubuntu 16.04/bash
- In this case, the second parameter can be any Linux file
Pure bash, no external commands used:
#!/bin/bash
parent=$1
child=$2
[[ $child && $parent ]] || exit 2 # both arguments must be present
child_dir="${child%/*}" # get the dirname of child
if [[ $child_dir = $parent && -d $child ]]; then
echo YES
else
echo NO
fi
Works for sub-directories too:
parent=$1
child=$2
if [[ ${child/$parent/} != $child ]] ; then
echo "YES"
else
echo "NO"
fi
Similar to Barmar's answer but far more reliable than name comparisons:
if find "$parent" -samefile "$child" -printf 'Y\n' -quit | grep -qF Y; then
echo "contains '$child'"
fi
To be even safer, you can also follow symlinks to ensure that there's no way a recursive operation on $parent could damage $child or anything in it:
if find -L "$parent" -samefile "$child" -printf 'Y\n' -quit | grep -qF Y; then
echo "contains '$child' or link thereto"
fi
You can accomplish this in pure bash. Loop over every file in $1 and see if "$1/$2" is a dir, like so:
parent=$1
child=$(basename $2)
if [ -d $parent ] && [ -d $child ]; then
for child in $parent; do
if [ -d "$parent/$child" ]; then
echo "Yes"
else
echo "No"
fi
done
fi

Extending bash script to receive more parameters?

I have the following bash script, which is called trash.sh. In my script I request one parameter for from the user, simply a file name. And move the file to the Trash folder which is located in the user's home directory. If the directory doesn't exist, it simply creates one and then moves the file there. On the other hand, if the file doesn't exist, it informs the user.
#!/bin/bash
FILE=$1
FOLDER="$HOME/Trash"
ARGS=1
if [ $# -ne $ARGS ]
then
echo "Error: You are missing an argument!"
echo "Usage: ./trash.sh <file_name>"
else
if [ -s $FILE ]
then
if [ -d $FOLDER ]
then
mv -v $FILE $FOLDER
else
mkdir $FOLDER
mv -v $FILE $FOLDER
fi
else
echo "The file you have entered does not exist!"
fi
fi
Now, I want to extend my script in the followign ways, but I don't know how, because I am not that much experienced with bash scripting. First of all, I want to let the user enter more than one parameters, simply more then one file name, and move all the files in the Trash folder. If one or more of the files don't exist, it will inform the user which file or files don't exist. Simply, I want my script to recieve as many parameters as the user wants.
For example, if the user calls the script like this:
./trash.sh file1 file2 file3
and let's say that file2 doesn't exist I want the output to be.
file1 -> /home/user/Trash/file1
file3 -> /home/user/Trash/file3
file2 doesn't exist!
And lastly, I also want it to accept a parameter like this:
./trash *.txt
Simply, which will move all the files that end with .txt extension. If someone could help me achieve those things/extend my script, I would be glad.
[ $# = 0 ] && { echo "Usage: $0 file [...]" >&2; exit 1; }
FOLDER="$HOME/Trash"
mkdir -p "$FOLDER" || exit 1
for file in "$#"
do
if [ -s "$file ]
then mv -v "$file" "$FOLDER"
else echo "$0: $file does not exist" >&2
fi
done
You can detect zero arguments and issue a usage message before the loop. Using "$#" is crucial to working with filenames containing blanks, etc; ditto with using "$file" to reference the files. Using mkdir -p does not fail if the target directory already exists; it does fail if the directory can't be created or if a file with the given name exists (instead of a directory). Note that the error messages both contain $0; note that the 'file not found' message specifies the file name.
As discussed in the comments, you can simplify this in two steps:
[ $# = 0 ] && { echo "Usage: $0 file [...]" >&2; exit 1; }
FOLDER="$HOME/Trash"
mkdir -p "$FOLDER" || exit 1
for file in "$#"
do mv -v "$file" "$FOLDER"
done
and then:
[ $# = 0 ] && { echo "Usage: $0 file [...]" >&2; exit 1; }
FOLDER="$HOME/Trash"
mkdir -p "$FOLDER" || exit 1
mv -v "$#" "$FOLDER"
Note that if you delete the makefiles in three directories, only one of them will be in the Trash.

sh: Test for existence of files

How does one test for the existence of files in a directory using bash?
if ... ; then
echo 'Found some!'
fi
To be clear, I don't want to test for the existence of a specific file. I would like to test if a specific directory contains any files.
I went with:
(
shopt -s dotglob nullglob
existing_files=( ./* )
if [[ ${#existing_files[#]} -gt 0 ]] ; then
some_command "${existing_files[#]}"
fi
)
Using the array avoids race conditions from reading the file list twice.
From the man page:
-f file
True if file exists and is a regular file.
So:
if [ -f someFileName ]; then echo 'Found some!'; fi
Edit: I see you already got the answer, but for completeness, you can use the info in Checking from shell script if a directory contains files - and lose the dotglob option if you want hidden files ignored.
I typically just use a cheap ls -A to see if there's a response.
Pseudo-maybe-correct-syntax-example-ahoy:
if [[ $(ls -A my_directory_path_variable ) ]] then....
edit, this will work:
myDir=(./*) if [ ${#myDir[#]} -gt 1 ]; then echo "there's something down here"; fi
You can use ls in an if statement thus:
if [[ "$(ls -a1 | egrep -v '^\.$|^\.\.$')" = "" ]] ; then echo empty ; fi
or, thanks to ikegami,
if [[ "$(ls -A)" = "" ]] ; then echo empty ; fi
or, even shorter:
if [[ -z "$(ls -A)" ]] ; then echo empty ; fi
These basically list all files in the current directory (including hidden ones) that are neither . nor ...
If that list is empty, then the directory is empty.
If you want to discount hidden files, you can simplify it to:
if [[ "$(ls)" = "" ]] ; then echo empty ; fi
A bash-only solution (no invoking external programs like ls or egrep) can be done as follows:
emp=Y; for i in *; do if [[ $i != "*" ]]; then emp=N; break; fi; done; echo $emp
It's not the prettiest code in the world, it simply sets emp to Y and then, for every real file, sets it to N and breaks from the for loop for efficiency. If there were zero files, it stays as Y.
Try this
if [ -f /tmp/foo.txt ]
then
echo the file exists
fi
ref: http://tldp.org/LDP/abs/html/fto.html
you may also want to check this out: http://tldp.org/LDP/abs/html/fto.html
How about this for whether directory is empty or not
$ find "/tmp" -type f -exec echo Found file {} \;
#!/bin/bash
if [ -e $1 ]; then
echo "File exists"
else
echo "Files does not exist"
fi
I don't have a good pure sh/bash solution, but it's easy to do in Perl:
#!/usr/bin/perl
use strict;
use warnings;
die "Usage: $0 dir\n" if scalar #ARGV != 1 or not -d $ARGV[0];
opendir my $DIR, $ARGV[0] or die "$ARGV[0]: $!\n";
my #files = readdir $DIR;
closedir $DIR;
if (scalar #files == 2) { # . and ..
exit 0;
}
else {
exit 1;
}
Call it something like emptydir and put it somewhere in your $PATH, then:
if emptydir dir ; then
echo "dir is empty"
else
echo "dir is not empty"
fi
It dies with an error message if you give it no arguments, two or more arguments, or an argument that isn't a directory; it's easy enough to change if you prefer different behavior.
# tested on Linux BASH
directory=$1
if test $(stat -c %h $directory) -gt 2;
then
echo "not empty"
else
echo "empty"
fi
For fun:
if ( shopt -s nullglob ; perl -e'exit !#ARGV' ./* ) ; then
echo 'Found some!'
fi
(Doesn't check for hidden files)

Resources