3

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 3

6

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

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

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
7
  • I realize that you aren’t the original author of this code. But the original author seems to have left the building, and you’ve taken over, so I’ll treat you as if you were the author, and put these comments here. (1) You clearly understand what functions are. They are great for reducing redundancy. But you don’t use them well. Over and over we see commands repeated, once for dir1 and again for dir2, or for slightly different situations. When you’re doing the same thing(s) repeatedly, you should consider putting them into a function. … (Cont’d) Commented Mar 13, 2021 at 2:02
  • (Cont’d) …  (1b) As far as I can tell, Proc1 is basically just Proc2 | 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 an if-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) Commented Mar 13, 2021 at 2:02
  • (Cont’d) …  You should definitely not do it if the script isn’t running on a tty, or if $TERM is not xterm.  (5b) And why do you have the functionality in two layers? PrintInTitle isn’t called anywhere but PrintJustInTitle; 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 is sort_command sorting? What is uniq_command doing?  … (Cont’d) Commented Mar 13, 2021 at 2:02
  • (Cont’d) … Why does Proc1 call sort_command and uniq_command when Proc2 doesn’t? What are line1 and line2? What can you ever get "$current_line" = "$previous_line"? What are skip and found_previous? Why do you test seconds_difference for 1 or -1 (but not 0)? 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. If dir1 is non-null or dir2 is non-null, do something; otherwise, if dir1 is non-null and dir2 is non-null, do something; … (Cont’d) Commented Mar 13, 2021 at 2:03
  • (Cont’d) …  otherwise, if dir1 is null or dir2 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) Commented Mar 13, 2021 at 2:03
0

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

3
  • 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). Commented Apr 21, 2015 at 17:07
  • 2
    Then 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. Commented Apr 21, 2015 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. Commented Apr 21, 2015 at 17:23

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.