Last active
August 25, 2024 11:30
-
-
Save vbz-repo/b012cc7ca5d04304e6b1caa4cbfc4638 to your computer and use it in GitHub Desktop.
Comparing the metadata of two files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Compare the metadata of two files: | |
| # file type | |
| # permission bits in octal | |
| # raw mode in hex | |
| # user ID of owner | |
| # group ID of owner | |
| # user name of owner | |
| # group name of owner | |
| # total size, in bytes | |
| # number of hard links | |
| # inode number | |
| # time of last access, seconds since Epoch | |
| # time of last data modification, seconds since Epoch | |
| # time of last status change, seconds since Epoch | |
| # time of file birth, seconds since Epoch; 0 if unknown | |
| # extended attributes | |
| # | |
| # BSD Zero Clause License | |
| # | |
| # Copyright (C) 2024 by Valdis Bluzma | |
| # | |
| # Permission to use, copy, modify, and/or distribute this software for any | |
| # purpose with or without fee is hereby granted. | |
| # | |
| # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | |
| # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | |
| # AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |
| # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM | |
| # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR | |
| # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | |
| # PERFORMANCE OF THIS SOFTWARE. | |
| # | |
| # TODO: | |
| # 2024-07-11 Add Access Control Lists (ACLs) permissions for comparison. | |
| # | |
| # URL: <https://gist.github.com/vbz-repo/b012cc7ca5d04304e6b1caa4cbfc4638> | |
| # The last modified date is used as the script version. | |
| readonly LAST_MODIFIED='2024-08-25 13:38:21' | |
| readonly SCRIPT_NAME='compare-file-metadata' | |
| # ------------------------------ | |
| # Output error information to STDERR. | |
| # Globals: | |
| # SCRIPT_NAME | |
| # Arguments: | |
| # $*: error message | |
| # Outputs: | |
| # STDERR: error information | |
| # ------------------------------ | |
| error() { | |
| printf "%s: %b\n" "${SCRIPT_NAME}" "${*:-"Unknown Error"}" >&2 | |
| } | |
| # ------------------------------ | |
| # Outputs: | |
| # STDOUT: script website | |
| # ------------------------------ | |
| print_website() { | |
| local ws='https://gist.github.com/vbz-repo/b012cc7ca5d04304e6b1caa4cbfc4638' | |
| printf 'Web site: <%s>' "${ws}" | |
| } | |
| # ------------------------------ | |
| # Globals: | |
| # SCRIPT_NAME | |
| # LAST_MODIFIED | |
| # Outputs: | |
| # STDOUT: script name, last modified date | |
| # ------------------------------ | |
| print_last_modified() { | |
| printf '%s, last modified: %s' "${SCRIPT_NAME}" "${LAST_MODIFIED}" | |
| } | |
| # ------------------------------ | |
| # Display script usage information. | |
| # Globals: | |
| # SCRIPT_NAME | |
| # Arguments: | |
| # None | |
| # Outputs: | |
| # STDOUT: script usage information | |
| # ------------------------------ | |
| usage() { | |
| cat <<EOF | |
| Usage: | |
| ${SCRIPT_NAME} --help | |
| ${SCRIPT_NAME} --version | |
| ${SCRIPT_NAME} [OPTION]... FILE1 FILE2 | |
| Compare the metadata of FILE1 and FILE2: | |
| file type | |
| permission bits in octal | |
| raw mode in hex | |
| user ID of owner | |
| group ID of owner | |
| user name of owner | |
| group name of owner | |
| total size, in bytes | |
| number of hard links | |
| inode number | |
| time of last access, seconds since Epoch | |
| time of last data modification, seconds since Epoch | |
| time of last status change, seconds since Epoch | |
| time of file birth, seconds since Epoch; 0 if unknown | |
| extended attributes | |
| Options: | |
| -h, --hlinks include number of hard links for comparison | |
| -i, --inode include inode number for comparison | |
| -X, --atime include access time for comparison | |
| -Y, --mtime include modification time for comparison | |
| -Z, --ctime include time of status change for comparison | |
| -W, --crtime include birth (or creation) time | |
| --xattrs include extended attributes for comparison | |
| --suppress-common-lines do not output common lines | |
| -v, --verbose report additional details: | |
| the metadata is identical or different | |
| --help display help and exit | |
| --version output version information and exit | |
| EOF | |
| } | |
| # ------------------------------ | |
| # Outputs: | |
| # STDOUT: the script version and license information | |
| # ------------------------------ | |
| version() { | |
| cat <<EOF | |
| $(print_last_modified) | |
| Copyright (C) 2024 by Valdis Bluzma | |
| License: 0BSD BSD Zero Clause License | |
| For more information, please refer to <https://spdx.org/licenses/0BSD.html> | |
| $(print_website) | |
| EOF | |
| } | |
| # ------------------------------ | |
| # Outputs: | |
| # STDOUT: help information | |
| # ------------------------------ | |
| help() { | |
| cat <<EOF | |
| $(print_last_modified) | |
| $(usage) | |
| $(print_website) | |
| EOF | |
| } | |
| # ------------------------------ | |
| # Check for required external commands. | |
| # Globals: | |
| # FUNCNAME | |
| # Arguments: | |
| # $@: the names of required commands | |
| # Outputs: | |
| # STDERR: if a command is not found, then output the error message | |
| # Returns: | |
| # 1 if a command is not found | |
| # ------------------------------ | |
| check_dependencies() { | |
| # no arguments | |
| if (( $# == 0 )); then | |
| error "${FUNCNAME[0]}: missing argument" | |
| return 1 | |
| fi | |
| for cmd in "$@"; do | |
| if ! hash -- "${cmd}" >/dev/null 2>&1; then | |
| error "the command '${cmd}' is not found" | |
| return 1 | |
| fi | |
| done | |
| return 0 | |
| } | |
| # ------------------------------ | |
| # Get all extended attributes. | |
| # Globals: | |
| # FUNCNAME | |
| # Arguments: | |
| # $1: file or directory | |
| # Outputs: | |
| # STDOUT: formatted string of file attributes | |
| # ------------------------------ | |
| getxattrs() { | |
| # Check for 'getfattr' external command. | |
| local -ar dependencies=(getfattr) | |
| check_dependencies "${dependencies[@]}" || return 1 | |
| # Check the number of positional parameters. | |
| if (( $# != 1 )); then | |
| local error_message | |
| if (( $# == 0 )); then # no arguments | |
| error_message='missing argument' | |
| else | |
| error_message='too many arguments' | |
| fi | |
| error "${FUNCNAME[0]}: ${error_message}" | |
| return 1 | |
| fi | |
| # Check if the file exists. | |
| local file="$1" | |
| if [[ ! -e "${file}" ]]; then | |
| error "'${file}': the file or directory does not exist" | |
| return 1 | |
| fi | |
| local data | |
| local -a getfattr_options | |
| getfattr_options+=(--dump) | |
| getfattr_options+=(--match=-) # "-": for including all attributes | |
| getfattr_options+=(--no-dereference) | |
| getfattr_options+=(--absolute-names) | |
| getfattr_options+=(--encoding=hex) | |
| getfattr_options+=(--) # "--": end of command line options | |
| local -r getfattr_options # set readonly attribute | |
| data="$(getfattr "${getfattr_options[@]}" "${file}")" || return 1 | |
| if [[ -n "${data}" ]]; then # file with extended attributes | |
| # Check the output format of 'getfattr'. | |
| if [[ ! "${data}" =~ ^"# file: ".+$ || ! "${data}" =~ ($'\n')+ ]]; then | |
| error 'extended attributes:' \ | |
| "the output format of 'getfattr' is incorrect:\n'${data}'" | |
| return 1 | |
| fi | |
| # Remove the first line of output. | |
| data="xattrs:\n${data#*$'\n'}" | |
| else | |
| data="xattrs:-" # without extended attributes | |
| fi | |
| printf "%s" "${data}" | |
| return 0 | |
| } | |
| # ------------------------------ | |
| # main program | |
| # Globals: | |
| # None | |
| # Arguments: | |
| # $@: the command line options and arguments (FILE1, FILE2) | |
| # Outputs: | |
| # STDOUT: the comparison result of the 'diff' command in a side-by-side format | |
| # STDERR: all error messages | |
| # Returns: | |
| # The exit status is the same as the 'diff' command status. | |
| # 0 if metadata (FILE1, FILE2) are the same | |
| # 1 if different | |
| # 2 if trouble | |
| # ------------------------------ | |
| main() { | |
| # variables for the command line options | |
| local hlinks # number of hard links (-h, --hlinks) | |
| local inode # inode number (-i, --inode) | |
| local atime # access time (-X, --atime) | |
| local mtime # modification time (-Y, --mtime) | |
| local ctime # time of status change (-Z, --ctime) | |
| local crtime # creation or birth time (-W, --crtime) | |
| local xattrs # extended attributes (--xattrs) | |
| # Do not output common lines - the command line option. | |
| # (--suppress-common-lines) | |
| local suppress_common_lines | |
| # Output an additional message about the comparison result: | |
| # the metadata is identical or not. (-v, --verbose) | |
| local verbose | |
| # Declare the array of positional arguments containing files to compare, | |
| # not parameters like options/flags. | |
| local -a arguments | |
| local -i arguments_count | |
| # the array for the metadata of files | |
| local -a arguments_metadata | |
| # the default options for the 'diff' command | |
| # "-y" or "--side-by-side": output in two columns | |
| local -a diff_options | |
| diff_options=(-y --width=100) | |
| # default file metadata attributes | |
| local attributes | |
| attributes+="type:%F\n" | |
| attributes+="permission:%#a\n" | |
| attributes+="raw:%f\n" | |
| attributes+="uid:%u\n" | |
| attributes+="gid:%g\n" | |
| attributes+="user:%U\n" | |
| attributes+="group:%G\n" | |
| attributes+="size:%s\n" | |
| local error_message | |
| local -i exit_status | |
| # Check required external commands. | |
| local -ar dependencies=(cat stat diff) | |
| check_dependencies "${dependencies[@]}" || exit 2 | |
| # no arguments | |
| if (( $# == 0 )); then | |
| error 'missing argument' | |
| usage | |
| exit 2 | |
| fi | |
| # Parse command-line arguments. | |
| while (( $# > 0 )); do | |
| case "$1" in | |
| --help) | |
| help | |
| exit 0 | |
| ;; | |
| --version) | |
| version | |
| exit 0 | |
| ;; | |
| -h | --hlinks) # number of hard links | |
| hlinks='true' | |
| shift | |
| ;; | |
| -i | --inode) # inode number | |
| inode='true' | |
| shift | |
| ;; | |
| -X | --atime) # file access time | |
| atime='true' | |
| shift | |
| ;; | |
| -Y | --mtime) # file modification time | |
| mtime='true' | |
| shift | |
| ;; | |
| -Z | --ctime) # file change time | |
| ctime='true' | |
| shift | |
| ;; | |
| -W | --crtime) # file creation time | |
| crtime='true' | |
| shift | |
| ;; | |
| --xattrs) # extended attributes | |
| xattrs='true' | |
| shift | |
| ;; | |
| --suppress-common-lines) # don't output common lines | |
| suppress_common_lines='true' | |
| shift | |
| ;; | |
| -v | --verbose) | |
| verbose='true' | |
| shift | |
| ;; | |
| --* | -*) | |
| error "invalid option '$1'" | |
| usage | |
| exit 2 | |
| ;; | |
| *) | |
| # Save positional arguments such as FILE1 or FILE2. | |
| arguments+=("$1") | |
| shift # past argument | |
| ;; | |
| esac | |
| done | |
| # Check the number of positional arguments. | |
| arguments_count=${#arguments[@]} | |
| if (( arguments_count != 2 )); then | |
| if (( arguments_count < 2 )); then | |
| error_message='missing argument' | |
| else | |
| error_message='too many arguments' | |
| fi | |
| error "${error_message}" | |
| usage | |
| exit 2 | |
| fi | |
| exit_status=0 | |
| # Check if files exist. | |
| for file in "${arguments[@]}"; do | |
| if [[ ! -e "${file}" ]]; then | |
| error "'${file}': the file or directory does not exist" | |
| exit_status=2 | |
| fi | |
| done | |
| (( exit_status == 2 )) && exit 2 | |
| # Check and process the command line options: | |
| # hlinks, inode, atime, mtime, ctime, crtime. | |
| if [[ "${hlinks}" == 'true' ]]; then | |
| attributes+="hlinks:%h\n" | |
| fi | |
| if [[ "${inode}" == 'true' ]]; then | |
| attributes+="inode:%i\n" | |
| fi | |
| if [[ "${atime}" == 'true' ]]; then | |
| attributes+="atime:%X\n" | |
| fi | |
| if [[ "${mtime}" == 'true' ]]; then | |
| attributes+="mtime:%Y\n" | |
| fi | |
| if [[ "${ctime}" == 'true' ]]; then | |
| attributes+="ctime:%Z\n" | |
| fi | |
| if [[ "${crtime}" == 'true' ]]; then | |
| attributes+="crtime:%W\n" | |
| fi | |
| for i in "${!arguments[@]}"; do | |
| arguments_metadata[i]="$(stat \ | |
| --printf="${attributes}" "${arguments[$i]}")\n" || exit 2 | |
| # extended attributes | |
| if [[ "${xattrs}" == 'true' ]]; then | |
| # Get a dump of all extended attributes. | |
| arguments_metadata[i]+="$(getxattrs "${arguments[$i]}")\n" || exit 2 | |
| fi | |
| done | |
| # Do not output common lines. | |
| if [[ "${suppress_common_lines}" == 'true' ]]; then | |
| diff_options+=(--suppress-common-lines) | |
| fi | |
| # Compare with the 'diff' command. | |
| diff "${diff_options[@]}" \ | |
| <(printf '%b' "${arguments_metadata[0]}") \ | |
| <(printf '%b' "${arguments_metadata[1]}") | |
| exit_status=$? | |
| # Output an additional message about the comparison result. | |
| if [[ "${verbose}" == 'true' ]]; then | |
| (( exit_status == 0 )) && echo 'The metadata of the files are identical' | |
| (( exit_status == 1 )) \ | |
| && echo 'The metadata of the files are NOT identical' | |
| fi | |
| return ${exit_status} | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment