# DIFF with Modification date - a .sh (dash; bash; zsh - compatible)
# "diff utility"-like script that can compare files in two directory
# trees by path, size and modification date
Disclaimer:
By using this program you are assuming full responsibility (and the
author of this program shall not be held liable) for any data loss
or damage that may result from the use or misuse of this program.
Proc1 () {
new="$line"
first_remaining_part="${new%"///////"}"
time="${first_remaining_part#"///////"}"
first_part="${new%%" ///////"}"
#last_part="${new##"///////"}"
seconds_since_epoch="${new##*" "}"
#convert seconds_since_epoch to base 10 (remove 0's padding):
temp="${seconds_since_epoch#"0"*[1-9]}"
zeros="${seconds_since_epoch%"$temp"}"
zeros="${zeros%?}" #Remove last non-0 digit
seconds_since_epoch="${seconds_since_epoch#"$zeros"}"
time_zone="${time##*" "}"
time="${time%"."*}"
new="$first_part ///// $time $time_zone ///// $seconds_since_epoch"
printf '%s\n' "$new"
}
Proc2 () {
cd "$dir1" >/dev/null 2>/dev/null
count_files=0
{
IFS="
"
set -f
#FilePath / FileType / FileSize / FileTime / SecondsSinceEpoch
if [ "$OS" = "Linux" -o "$OS" = "Windows" ]; then
find . -not -type d -printf '%p ///// <_ ///// %s ///////%TY-%Tm-%Td %TT %Tz///////' -exec stat --printf ' %Y\n' {} ; 2>/dev/null|
{
while read line; do {
count_files=$((count_files + 1));
PrintJustInTitle "Analyzing file $count_files...";
Proc1;
}; done;
printf "$count_files">$RAMLocation/diffm_count.txt;
}
elif [ "$OS" = "Mac" ]; then
gfind . -not -type d -printf '%p ///// <_ ///// %s ///////%TY-%Tm-%Td %TT %Tz///////' -exec gstat --printf ' %Y\n' {} ; 2>/dev/null|
{
while read line; do {
count_files=$((count_files + 1));
PrintJustInTitle "Analyzing file $count_files...";
Proc1;
}; done;
printf "$count_files">$RAMLocation/diffm_count.txt;
}
fi
unset IFS
set +f
}
cd "$initial_dir"
}
Proc3 () {
cd "$dir2" >/dev/null 2>/dev/null
#Get previous line count value from diffm_count.txt:
read count_files<"$RAMLocation/diffm_count.txt"
#Validate count_files as a number:
count_files=$((count_files))
{
IFS="
"
set -f
#FilePath / FileType / FileSize / FileTime / SecondsSinceEpoch
if [ "$OS" = "Linux" -o "$OS" = "Windows" ]; then
find . -not -type d -printf '%p ///// >_ ///// %s ///////%TY-%Tm-%Td %TT %Tz///////' -exec stat --printf ' %Y\n' {} ; 2>/dev/null|
{
while read line; do {
count_files=$((count_files + 1));
PrintJustInTitle "Analyzing file $count_files...";
Proc1;
}; done;
printf "$count_files">$RAMLocation/diffm_count.txt;
}
elif [ "$OS" = "Mac" ]; then
gfind . -not -type d -printf '%p ///// >_ ///// %s ///////%TY-%Tm-%Td %TT %Tz///////' -exec gstat --printf ' %Y\n' {} ; 2>/dev/null|
{
while read line; do {
count_files=$((count_files + 1));
PrintJustInTitle "Analyzing file $count_files...";
Proc1;
}; done;
printf "$count_files">$RAMLocation/diffm_count.txt;
}
fi
unset IFS
set +f
}
#Get total line count value from diffm_count.txt:
read count_files<"$RAMLocation/diffm_count.txt"
#Validate count_files as a number:
count_files=$((count_files))
#"." in front of the number is for displaying it as first line after the sort operation:
printf ".$count_files"
cd "$initial_dir"
}
GetOS () {
OS_kernel_name=$(uname -s)
case "$OS_kernel_name" in
"Linux")
eval $1="Linux"
;;
"Darwin")
eval $1="Mac"
;;
"CYGWIN"*|"MSYS"*|"MINGW"*)
eval $1="Windows"
;;
"")
eval $1="unknown"
;;
*)
eval $1="other"
;;
esac
}
PrintInTitle () {
printf "\033]0;%s\007" "$1"
}
PrintJustInTitle () {
PrintInTitle "$1">/dev/tty
}
trap1 () {
CleanUp
printf "\nAborted.\n">/dev/tty
}
CleanUp () {
#Clear the title:
PrintJustInTitle ""
#Restore "INTERRUPT" (CTRL-C) and "TERMINAL STOP" (CTRL-Z) signals:
trap - INT
trap - TSTP
}
GetSetRAMLocation () {
GetOS OS
if [ "$OS" = "Linux" ]; then
RAMLocation='/dev/shm'
elif [ "$OS" = "Mac" ]; then
RAMLocation='/Volumes/RamDiskDIFFM'
sizeInMB = "1"
if [ ! -e "/Volumes/RamDiskDIFFM" ]; then
diskutil partitionDisk $(hdiutil attach -nomount ram://$((2048sizeInMB))) 1 GPTFormat APFS 'RamDiskDIFFM' '100%'&&{
printf "">"$RAMLocation/diffm_count.txt"&&{
error="false"
}||{
printf '\n%s\n\n' "Error: Could not write to RAM ("$RAMLocation/diffm_count.txt"). Write to current directory instead? [ Yes (=continue) / No (=Enter=abort) ]">/dev/tty
read answer
case "$answer" in
"Y"|"y"|"Yes"|"yes")
answer="yes"
;;
"N"|"n"|"No"|"no")
answer="no"
;;
"")
answer="no"
;;
)
answer="no"
;;
esac
if [ "$answer" = "yes" ]; then
RAMLocation="$initial_dir"
error="false"
elif [ "$answer" = "no" ]; then
printf "\nAborted.\n\n">/dev/tty
error="true"
fi
}
}||{
printf '\n%s\n\n' "ERROR: Could not create RAM Volume!">/dev/tty
error="true"
}
fi
if [ "$error" = "true" ]; then
CleanUp
exit 1
fi
elif [ "$OS" = "Windows" ]; then
#On Windows, the RAMLocation variable is not currently designed to point to a RAM Drive path:
RAMLocation="$initial_dir"
elif [ $OS" = "other ]; then
#On other operating systems, the RAMLocation variable is not currently designed to point to a RAM Drive path:
RAMLocation="$initial_dir"
else
printf '\n%s\n\n' "ERROR: Could not get OS!">/dev/stderr
CleanUp
exit 1
fi
}
DestroyArray () {
eval array_len=$(($1_0))
j=0;
while [ "$j" -lt "$array_len" ]; do
j=$((j+1))
unset $1\_$j
done
unset $1\_0
}
DisplayHelp () {
printf "\n"
printf "diffm - DIFF with Modification date\n"
printf "\n"
printf " What it does:\n"
printf " - compares the files (recursively) in the two provided directory tree paths (<dir_tree1> and <dir_tree2>) by:\n"
printf " 1. Path\n"
printf " 2. Size\n"
printf " 3. Modification date\n"
printf " Syntax:\n"
printf " <caller_shell> '/path/to/diffm.sh' <dir_tree1> <dir_tree2> [flags]\n"
printf " - where:\n"
printf " - <caller_shell> can be any of the shells: dash, bash, zsh, or any other shell compatible with the "dash" shell syntax\n"
printf " - '/path/to/diffm.sh' represents the path of this script\n"
printf " - <dir_tree1> and <dir_tree2> represent the directory trees to be compared\n"
printf " - [flags] can be:\n"
printf " --help or -h\n"
printf " Displays this help information\n"
printf " Output:\n"
printf " - lines starting with '<' signify files from <dir_tree1>\n"
printf " - lines starting with '>' signify files from <dir_tree2>\n"
printf " Notes:\n"
printf " - only files in the two provided directory tree paths are compared, not also directories\n"
printf "\n"
}
##################
MAIN START
##################
GetOS OS
if [ "$OS" = "Linux" -o "$OS" = "Windows" ]; then
error1="false"
error2="false"
error3="false"
{ sort --version-sort --help >/dev/null 2>/dev/null; } || { error1="true"; }
{ stat --help >/dev/null 2>/dev/null; } || { error2="true"; }
{ find --help >/dev/null 2>/dev/null; } || { error3="true"; }
if [ "$error1" = "true" -o "$error2" = "true" -o "$error3" = "true" ]; then
{
printf "\n"
if [ "$error1" = "true" ]; then printf '%s' "ERROR: Could not run \"sort --version-sort\" (necessary in order for this script to function correctly)!"; fi
if [ "$error2" = "true" ]; then printf '%s' "ERROR: Could not run \"stat\" (necessary in order for this script to function correctly)"; fi
if [ "$error3" = "true" ]; then printf '%s' "ERROR: Could not run \"find\" (necessary in order for this script to function correctly)"; fi
printf "\n"
}>/dev/stderr
exit
else
sort_command="sort --version-sort"
fi
elif [ "$OS" = "Mac" ]; then
error1="false"
error2="false"
error3="false"
{ gsort --version-sort --help >/dev/null 2>/dev/null; } || { error1="true"; }
{ gstat --help >/dev/null 2>/dev/null; } || { error2="true"; }
{ gfind --help >/dev/null 2>/dev/null; } || { error3="true"; }
if [ "$error1" = "true" -o "$error2" = "true" -o "$error3" = "true" ]; then
{
printf "\n"
if [ "$error1" = "true" ]; then printf '%s' "ERROR: Could not run \"gsort --version-sort\" (necessary in order for this script to function correctly)!"; fi
if [ "$error2" = "true" ]; then printf '%s' "ERROR: Could not run \"gstat\" (necessary in order for this script to function correctly)"; fi
if [ "$error3" = "true" ]; then printf '%s' "ERROR: Could not run \"gfind\" (necessary in order for this script to function correctly)"; fi
printf "\n"
printf "\n"
printf '%s\n' "You can install them by installing \"homebrew\" and then GNU \"coreutils\":"
printf '%s\n' "sudo ruby -e \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\""
printf '%s\n' "sudo brew install coreutils"
}>/dev/stderr
exit
else
sort_command="gsort --version-sort"
fi
elif [ "$OS" = "unknown" ]; then
printf '\n%s\n\n' "ERROR: Could not get OS!">/dev/stderr
CleanUp
exit 1
elif [ "$OS" = "other" ]; then
printf '\n%s\n\n' "ERROR: OS currently not supported!">/dev/stderr
CleanUp
exit 1
fi
#Get the program parameters into the array "params":
params_count=0
for i; do
params_count=$((params_count+1))
eval params_$params_count="$i"
done
params_0=$((params_count))
if [ "$params_0" = "0" ]; then #if no parameters are provided: display help
DisplayHelp
CleanUp && exit 0
fi
#Create a flags array. A flag denotes special parameters:
help_flag="0"
i=1;
j=0;
while [ "$i" -le "$((params_0))" ]; do
eval params_i="${params_$i}"
case "${params_i}" in
"--help" | "-h" )
help_flag="1"
;;
* )
j=$((j+1))
eval selected_params_$j="$params_i"
;;
esac
i=$((i+1))
done
selected_params_0=$j
#Rebuild params array:
DestroyArray params
for i in $(seq 1 $selected_params_0); do
eval params_$i="${selected_params_$i}"
done
params_0=$selected_params_0
if [ "$help_flag" = "1" ]; then
DisplayHelp
else
#Check program arguments:
if [ "$params_0" -lt "2" ]; then
printf '\n%s\n' "ERROR: To few program parameters provided (expected two: <dir_tree1> and <dir_tree2>)!">/dev/stderr
exit 1
elif [ "$params_0" -gt "2" ]; then
printf '\n%s\n' "ERROR: To many program parameters provided (expected two: <dir_tree1> and <dir_tree2>)!">/dev/stderr
exit 1
else #two program arguments are provided:
mask1="$params_1"
mask2="$params_2"
fi
#If two program arguments are provided (<dir_tree1> and <dir_tree2>) proceed to checking them:
initial_dir="$PWD" #Store initial dir
error_encountered="false"
[ -e "$params_1" -a -d "$params_1" ] && cd "$params_1" >/dev/null 2>/dev/null && {
dir1="$PWD"
cd "$initial_dir"
}||{
printf '\n%s\n' "ERROR: \"$params_1\" does not exist as a directory or is not accessible!">/dev/stderr
error_encountered="true"
}
printf "\n">/dev/tty
[ -e "$params_2" -a -d "$params_2" ] && cd "$params_2" >/dev/null 2>/dev/null && {
dir2="$PWD"
cd "$initial_dir"
}||{
printf '%s\n' "ERROR: \"$params_2\" does not exist as a directory or is not accessible!">/dev/stderr
error_encountered="true"
}
if [ "$error_encountered" = "true" ]; then
printf "\n">/dev/stderr
exit
fi
GetSetRAMLocation
#Trap "INTERRUPT" (CTRL-C) and "TERMINAL STOP" (CTRL-Z) signals:
trap 'trap1' INT
trap 'trap1' TSTP
#Proceed to <dir_tree1> and <dir_tree2> compare:
IFS="
"
set -f
unset count_total
unset previous_line
unset current_line
unset previous_case
skip="0"
count="0"
found_previous="false"
found_previous_last_line_case="false"
for l in $(
{
PrintJustInTitle "Loading file data, please wait...";
Proc2;
Proc3;
PrintJustInTitle "Sorting results, please wait...";
}|eval $sort_command;
); do
if [ -z "$count_total" ]; then
# #? is for removing the "." in front of the number:
count_total=$((${l#?}))
else
count=$((count + 1))
PrintJustInTitle "Analyzing file $count of $count_total"
if [ -z "$current_line" ]; then
#FilePath / FileType / FileSize / FileTime / SecondsSinceEpoch
#Get previous FileType = if from <dir_tree1> or <dir_tree2>:
previous_line_type="${l#*"///// "}"
#extract first character from $previous_line_type ("<" or ">"):
previous_line_type_temp="${previous_line_type#?}"
previous_line_type="${previous_line_type%"$previous_line_type_temp"}"
#Remove FileType from line:
previous_line="$l"
previous_line_file_path="${l%%" /////"*}"
previous_line_last_part="${previous_line#*"///// ""$previous_line_type""_ ///// "}"
previous_line="$previous_line_file_path /////$previous_line_last_part"
previous_line_file_size="${previous_line_last_part%%" /////"*}"
previous_line_seconds_since_epoch="${l##*" "}"
previous_line_file_time="${previous_line#*"///// "}"
previous_line_file_time="${previous_line_file_time%%" /////"*}"
current_line="$previous_line"
current_line_file_path="$previous_line_file_path"
current_line_file_size="$previous_line_file_size"
current_line_seconds_since_epoch="$previous_line_seconds_since_epoch"
current_line_type="$previous_line_type"
current_line_file_time="$previous_line_file_time"
else
#FilePath / FileType / FileSize / FileTime / SecondsSinceEpoch
previous_line="$current_line"
previous_line_file_path="$current_line_file_path"
previous_line_file_size="$current_line_file_size"
previous_line_seconds_since_epoch="$current_line_seconds_since_epoch"
previous_line_type="$current_line_type"
previous_line_file_time="$current_line_file_time"
#Get current FileType = if from <dir_tree1> or <dir_tree2>:
current_line_type="${l#*"///// "}"
#extract first character from $current_line_type ("<" or ">"):
current_line_type_temp="${current_line_type#?}"
current_line_type="${current_line_type%"$current_line_type_temp"}"
#Remove FileType from line:
current_line="$l"
current_line_file_path="${l%%" /////"*}"
current_line_last_part="${current_line#*"///// ""$current_line_type""_ ///// "}"
current_line="$current_line_file_path /////$current_line_last_part"
current_line_file_size="${current_line_last_part%%" /////"*}"
current_line_seconds_since_epoch="${l##*" "}"
current_line_file_time="${current_line#*"///// "}"
current_line_file_time="${current_line_file_time%%" /////"*}"
if [ ! "$skip" = "$count" ]; then
if [ "$found_previous" = "false" ]; then
seconds_difference=$(($current_line_seconds_since_epoch - $previous_line_seconds_since_epoch))
if [ \
\( "$current_line" = "$previous_line" \) -o \
\( \
\( "$current_line_file_path" = "$previous_line_file_path" \) -a \
\( "$current_line_file_size" = "$previous_line_file_size" \) -a \
\( "$seconds_difference" = "1" -o "$seconds_difference" = "-1" \) \
\) \
]; then
found_previous="true"
found_previous_last_line_case="false"
skip=$((count+1))
else
printf '%s\n' "$previous_line_type $previous_line_file_path - ""Size: ""$previous_line_file_size"" Bytes"" - ""Modified Date: ""$previous_line_file_time"
found_previous="false"
found_previous_last_line_case="true"
fi
else
printf '%s\n' "$previous_line_type $previous_line_file_path - ""Size: ""$previous_line_file_size"" Bytes"" - ""Modified Date: ""$previous_line_file_time"
found_previous="false"
found_previous_last_line_case="true"
fi
else
found_previous="false"
found_previous_last_line_case="true"
fi
fi
fi
done
#Treat last case sepparatelly:
if [ "$found_previous_last_line_case" = "true" ]; then
printf '%s\n' "$current_line_type $current_line_file_path - ""Size: ""$current_line_file_size"" Bytes"" - ""Modified Date: ""$current_line_file_time"
fi
unset IFS
set +f
fi
CleanUp
source/
andtarget/
are also both very important! (Without them, you will compare source and target directory names along with the child file names, so all file names will differ.) – peschü Feb 16 '19 at 14:10*deleting
, but files, which are in source only did show up. The slashes are easy to accidentally forget and then you get a plausible but wrong output. – user643011 Aug 27 '19 at 19:10rsync -n -a -i --delete /Volumes/share/AdvantageProxy/Development/bin/. /Volumes/share/AdvantageProxy/Deploy-QA-2020-02-21/bin/.
– Cognitiaclaeves Feb 24 '20 at 20:25