Diff directories using modification time (mtime) and size instead of contents

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?

Here is Solutions:

We have many solutions to this problem, But we recommend you to use the first solution because it is tested & true solution that will 100% work for you.

Solution 1

Use rsync, but tell it not to copy or remove any files.

rsync -a -nv --delete a/ b/

Solution 2

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:
    1. relative path
    2. size
    3. 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 ‘>
  • Notes:
    • Only files are compared, not also directories
    • Should work on Linux OS, Mac OS – without installing any additional tools

Solution 3

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).

Note: Use and implement solution 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply