Is there an option to have diff (-q) not look at file contents and instead just look at size and mtime? If not, is there a tool similar to this that has the option?
3 Answers
Use rsync
, but tell it not to copy or remove any files.
rsync -a -nv --delete a/ b/

- 829,060
The next script is an improvement of the answer from here:
#!/bin/sh
diffm.sh
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
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
}
DetectShell () {
eval $1="";
if [ -n "$BASH_VERSION" ]; then
eval $1="bash";
elif [ -n "$ZSH_VERSION" ]; then
eval $1="zsh";
elif [ "$PS1" = '$ ' ]; then
eval $1="dash";
else
eval $1="undetermined";
fi
}
PrintInTitle () {
printf "\033]0;%s\007" "$1"
}
PrintJustInTitle () {
PrintInTitle "$1">/dev/tty
}
trap1 () {
CleanUp
printf "\nAborted.\n">/dev/tty
}
CleanUp () {
#Restore "INTERRUPT" (CTRL-C) and "TERMINAL STOP" (CTRL-Z) signals:
trap - INT
trap - TSTP
#Restore Initial Directory:
cd "$initial_dir"
#Clear the title:
PrintJustInTitle ""
#Restore initial IFS:
#IFS=$old_IFS
unset IFS
#Set shell flags (enable globbing):
set +f
}
DisplayHelp () {
printf "\n"
printf "diffm - DIFF by Modification date\n"
printf "\n"
printf " What it does:\n"
printf " - compares the files 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 the files in the two provided directory tree paths are compared, not also the folders\n"
printf "\n"
}
Proc1 () {
{
{
cd "$initial_dir"
[ -n "$dir1" ] && { cd "$dir1"; PrintJustInTitle "Loading files in directory 1, please wait..."; }
eval $command1
cd "$initial_dir"
[ -n "$dir2" ] && { cd "$dir2"; PrintJustInTitle "Loading files in directory 2, please wait..."; }
eval $command2
cd "$initial_dir"
}|eval $sort_command;
}|eval $uniq_command;
}
Proc2 () {
cd "$initial_dir"
[ -n "$dir1" ] && { cd "$dir1"; PrintJustInTitle "Loading files in directory 1, please wait..."; }
eval $command1
cd "$initial_dir"
[ -n "$dir2" ] && { cd "$dir2"; PrintJustInTitle "Loading files in directory 2, please wait..."; }
eval $command2
cd "$initial_dir"
}
GetOS OS
DetectShell current_shell
OS_CASE=""
if [ "$OS" = "Linux" ]; then
OS_CASE="1"; # = use Linux OS commands
elif [ "$OS" = "Mac" ]; then
OS_CASE="2"; # = use Mac OS commands
else
#################################################################################
## IN CASE YOUR OS IS NOT LINUX OR MAC: ##
## MODIFY THE NEXT VARIABLE ACCORDING TO YOUR SYSTEM REQUIREMENTS (e.g.: ##
## "1" (use Linux OS commands) or "2" (use Mac OS commands)): ##
#################################################################################
OS_CASE="3"
fi
if [ "$current_shell" = "undetermined" ]; then
printf "\nWarning: This script was designed to work with dash, bash and zsh shells.\n\n">/dev/tty
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:
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 #Run program:
error1="false"
error2="false"
error3="false"
error4="false"
{ 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"; }
{ uniq --help >/dev/null 2>/dev/null; } || { error4="true"; }
if [ "$error1" = "true" -o "$error2" = "true" -o "$error3" = "true" -o "$error4" = "true" ]; then
{
printf "\n"
if [ "$error1" = "true" ]; then printf '%s' "ERROR: Could not run \"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
if [ "$error4" = "true" ]; then printf '%s' "ERROR: Could not run \"uniq\" (necessary in order for this script to function correctly)!"; fi
printf "\n"
}>/dev/stderr
exit
fi
#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
fi
initial_dir="$PWD" #Store initial dir
#If two program arguments are provided (<dir_tree1> and <dir_tree2>) proceed to checking them:
initial_dir="$PWD" #Store initial dir
dir1=""
dir2=""
file1=""
file2=""
error_encountered="false"
error1="false"
error2="false"
[ -e "$params_1" ] && {
if [ -d "$params_1" ]; then
cd "$params_1" >/dev/null 2>/dev/null && {
dir1="$PWD"
cd "$initial_dir"
} || {
error1="true"
}
elif [ ! -d "$params_1" ]; then
file1="$params_1"
fi
}||{
error2="true"
}
if [ "$error1" = "true" -o "$error2" = "true" ]; then
printf '\n%s\n' "ERROR: PARAMETER1: \"$params_1\" does not exist as a directory/file or is not accessible!">/dev/stderr
error_encountered="true"
fi
printf "\n">/dev/tty
error1="false"
error2="false"
[ -e "$params_2" ] && {
if [ -d "$params_2" ]; then
cd "$params_2" >/dev/null 2>/dev/null && {
dir2="$PWD"
cd "$initial_dir"
}||{
error1="true"
}
elif [ ! -d "$params_2" ]; then
file2="$params_2"
fi
}||{
error2="true"
}
if [ "$error1" = "true" -o "$error2" = "true" ]; then
printf '%s\n' "ERROR: PARAMETER2: \"$params_2\" does not exist as a directory/file or is not accessible!">/dev/stderr
error_encountered="true"
fi
if [ "$error_encountered" = "true" ]; then
printf "\n">/dev/stderr
exit
fi
## TYPE ///// PATH ///// SIZE ///// LAST TIME WRITE IN SECONDS ##
if [ "$OS_CASE" = "1" ]; then #Linux OS
if [ -n "$dir1" ]; then
command1='find . -not -type d -exec stat -c "< ///// %n ///// %s ///// %Y" {} \;'
else
command1_string="$(stat -c "< ///// %n ///// %s ///// %Y" "$file1")"
command1="printf '%s\n' \"\$command1_string\""
fi
if [ -n "$dir2" ]; then
command2='find . -not -type d -exec stat -c "> ///// %n ///// %s ///// %Y" {} \;'
else
command2_string="$(stat -c "> ///// %n ///// %s ///// %Y" "$file2")"
command2="printf '%s\n' \"\$command2_string\""
fi
command3='date -d @'
cd "$initial_dir"
sort_command="sort -k 3"
uniq_command="uniq -u -f 2"
elif [ "$OS_CASE" = "2" ]; then #Mac OS
if [ -n "$dir1" ]; then
command1='find . -not -type d -exec stat -f "< ///// %N ///// %z ///// %m" {} \;'
else
command1_string="$(stat -f "< ///// %N ///// %z ///// %m" "$file1")"
command1="printf '%s\n' \"\$command1_string\""
fi
if [ -n "$dir2" ]; then
command2='find . -not -type d -exec stat -f "> ///// %N ///// %z ///// %m" {} \;'
else
command2_string="$(stat -f "> ///// %N ///// %z ///// %m" "$file2")"
command2="printf '%s\n' \"\$command2_string\""
fi
command3='date -j -f %s '
cd "$initial_dir"
sort_command="sort -k 3"
uniq_command="uniq -u -f 2"
else
printf '\n%s\n\n' "Error: Unsupported OS!">/dev/stderr
exit 1
fi
#Trap "INTERRUPT" (CTRL-C) and "TERMINAL STOP" (CTRL-Z) signals:
trap 'trap1' INT
trap 'trap1' TSTP
old_IFS="$IFS" #Store initial IFS value
IFS="
"
set -f #Set shell flags (disable globbing):
found_previous="false"
count=0
skip=0
if [ -n "$dir1" -o -n "$dir2" ]; then
for line in $(\
if [ -n "$dir1" -a -n "$dir2" ]; then\
Proc1;\
elif [ -z "$dir1" -o -z "$dir2" ]; then\
Proc2;\
fi;\
); do
count=$((count+1))
PrintJustInTitle "Analyzing file $count..."
if [ -z "$current_line_file_type" ]; then
current_line="$line"
current_line_file_type="${line%%" ///// "*}"
current_line_file_mtime_in_seconds="${line##*" ///// "}"
current_line_file_type_path_and_size="${line%" ///// "*}"
current_line_file_size="${current_line_file_type_path_and_size##*" ///// "}"
current_line_file_type_path="${line%" ///// "*" ///// "*}"
current_line_file_path="${current_line_file_type_path#*" ///// "}"
else
previous_line="$current_line"
previous_line_file_type="$current_line_file_type"
previous_line_file_mtime_in_seconds="$current_line_file_mtime_in_seconds"
previous_line_file_type_path_and_size="$current_line_file_type_path_and_size"
previous_line_file_size="$current_line_file_size"
previous_line_file_type_path="$current_line_file_size"
previous_line_file_path="$current_line_file_path"
current_line="$line"
current_line_file_type="${line%%" ///// "*}"
current_line_file_mtime_in_seconds="${line##*" ///// "}"
current_line_file_type_path_and_size="${line%" ///// "*}"
current_line_file_size="${current_line_file_type_path_and_size##*" ///// "}"
current_line_file_type_path="${line%" ///// "*" ///// "*}"
current_line_file_path="${current_line_file_type_path#*" ///// "}"
if [ ! "$skip" = "$count" ]; then
if [ "$found_previous" = "false" ]; then
seconds_difference=$(($current_line_file_mtime_in_seconds - $previous_line_file_mtime_in_seconds))
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"
skip=$((count+1))
else
printf '%s\n' "$previous_line_file_type $previous_line_file_path - ""Size: ""$previous_line_file_size"" Bytes"" - ""Modified Date: ""$(eval $command3$previous_line_file_mtime_in_seconds)"
found_previous="false"
fi
else
printf '%s\n' "$previous_line_file_type $previous_line_file_path - ""Size: ""$previous_line_file_size"" Bytes"" - ""Modified Date: ""$(eval $command3$previous_line_file_mtime_in_seconds)"
found_previous="false"
fi
else
found_previous="false"
fi
fi
done
#Treat last case separately:
if [ "$count" -gt "0" ]; then
if [ "$found_previous" = "false" ]; then
printf '%s\n' "$current_line_file_type $current_line_file_path - ""Size: ""$current_line_file_size"" Bytes"" - ""Modified Date: ""$(eval $command3$current_line_file_mtime_in_seconds)"
fi
fi
else
line1=""
line2=""
for line in $(\
if [ -n "$dir1" -a -n "$dir2" ]; then\
Proc1;\
elif [ -z "$dir1" -o -z "$dir2" ]; then\
Proc2;\
fi;\
); do
if [ -z "$line1" ]; then
line1="$line"
line1_file_type="${line%%" ///// "*}"
line1_file_mtime_in_seconds="${line##*" ///// "}"
line1_file_type_path_and_size="${line%" ///// "*}"
line1_file_size="${line1_file_type_path_and_size##*" ///// "}"
line1_file_type_path="${line%" ///// "*" ///// "*}"
line1_file_path="${line1_file_type_path#*" ///// "}"
else
line2="$line"
line2_file_type="${line%%" ///// "*}"
line2_file_mtime_in_seconds="${line##*" ///// "}"
line2_file_type_path_and_size="${line%" ///// "*}"
line2_file_size="${line2_file_type_path_and_size##*" ///// "}"
line2_file_type_path="${line%" ///// "*" ///// "*}"
line2_file_path="${line2_file_type_path#*" ///// "}"
seconds_difference=$(($line2_file_mtime_in_seconds - $line1_file_mtime_in_seconds))
if [ \
\( "$line2_file_size" = "$line1_file_size" \) -a \
\( \
\( "$line2_file_mtime_in_seconds" = "$line1_file_mtime_in_seconds" \) -o \
\( "$seconds_difference" = "1" -o "$seconds_difference" = "-1" \) \
\) \
]; then
:;
else
printf '%s\n' "$line1_file_type $line1_file_path - ""Size: ""$line1_file_size"" Bytes"" - ""Modified Date: ""$(eval $command3$line1_file_mtime_in_seconds)"
printf '%s\n' "$line2_file_type $line2_file_path - ""Size: ""$line2_file_size"" Bytes"" - ""Modified Date: ""$(eval $command3$line2_file_mtime_in_seconds)"
fi
fi
done
fi
CleanUp
fi
- What it does:
- Compares the files (recursively) in the two directory tree paths provided as parameters (we denote them as:
<dir_tree1>
and<dir_tree2>
) by:- relative path
- size
- modification date
- if it finds differences: it lists the relative paths of the files that are different and their details (size and date modified):
- relative file paths for files in
<dir_tree1>
are prefixed with '<
' - relative file paths for files in
<dir_tree2>
are prefixed with '>
'
- relative file paths for files in
- Notes:
- Only files are compared, not also directories
- Should work on Linux OS, Mac OS - without installing any additional tools
Not with diff
- you don't need to look into the files for that - but just compare that information with stat
in shell, as in
if [[ $(stat -c%s_%Y file1) == $(stat -c%s_%Y file2) ]]
then echo equal
else echo different
fi
The stat
command provides information from the file's inode, -c
allows you to select the desired attributes (%s
and %Y
in your case).

- 14,222
-
I'm looking to get a picture of the way the directory trees differ including missing files/folders at any point in the structure (similar to what diff show). – Jack Apr 21 '15 at 17:07
-
2Then please adjust your question accordingly, any make also clear why
diff -r
(recursive diff) is not helpful in your case. Also provide input and an desired output sample, so that it's clear what you expect. – Janis Apr 21 '15 at 17:11 -
diff -q -r does not work because I want to save time if the files have the same place in the path, name and mtime. – Jack Apr 21 '15 at 17:23
dir1
and again fordir2
, or for slightly different situations. When you’re doing the same thing(s) repeatedly, you should consider putting them into a function. … (Cont’d) – G-Man Says 'Reinstate Monica' Mar 13 '21 at 02:02Proc1
is basically justProc2 | sort | uniq
— so why write the code twice? (2) Similarly, the strings///// %n ///// %s ///// %Y
and///// %N ///// %z ///// %m
are present four times each; that redundancy should be eliminated. (3) Similarly, sometimes you have the same code in both branches of anif
-then
-else
. (4) Why do you use/dev/stderr
? I believe that>&2
is more portable. (5) Unix/Linux programs & scripts should not be verbose by default. IMO, the display of status should be optional (opt-in). … (Cont’d) – G-Man Says 'Reinstate Monica' Mar 13 '21 at 02:02$TERM
is notxterm
. (5b) And why do you have the functionality in two layers?PrintInTitle
isn’t called anywhere butPrintJustInTitle
; why make them two separate functions? (5c) And why are you writing anything other than the status line to/dev/tty
? Error messages should be written to stderr. (6) There are comments in the code for the obvious stuff. You should add explanations for the tricky stuff. What field issort_command
sorting? What isuniq_command
doing? … (Cont’d) – G-Man Says 'Reinstate Monica' Mar 13 '21 at 02:02Proc1
callsort_command
anduniq_command
whenProc2
doesn’t? What areline1
andline2
? What can you ever get"$current_line" = "$previous_line"
? What areskip
andfound_previous
? Why do you testseconds_difference
for1
or-1
(but not0
)? And what, exactly, does the script do when you run it with one directory and one file (or two files) as arguments? (6b) And some of the conditions seem unnecessarily complex. Ifdir1
is non-null ordir2
is non-null, do something; otherwise, ifdir1
is non-null anddir2
is non-null, do something; … (Cont’d) – G-Man Says 'Reinstate Monica' Mar 13 '21 at 02:03dir1
is null ordir2
is null, do something. Under what (real-world) conditions does the second thing get done? Under what conditions do we fall through and not do anything? (Note that there are only three possible combinations of two Boolean values, where order doesn’t matter.) (7) Does your script handle filenames with spaces? (I suspect that it doesn’t; I’m not going to run it on my system until I understand it better.) (8) Have you considered processing the argument list once, and not copying it into an array-that-isn’t-an-array? … (Cont’d) – G-Man Says 'Reinstate Monica' Mar 13 '21 at 02:03Proc1
. (10) You should try to useeval
a *lot* less. (11) You broke theIFS
assignment — your code now sets it toCleanUp
that needs to be there isPrintJustInTitle
— everything else is shell-local settings that go away when the shell exits. (13) You should avoid using[
…-a
…]
and[
…-o
…]
. (14) You shouldn’t use-not
infind
unless you’re sure that it’s GNU find. (15) This answer would be better if it showed some example output.