Примечание о некоторых более легких реализациях (телефоны на базе Android, busybox, и т.д.): ps
не всегда имеет поддержку -p
переключатель, но можно выполнить поиск с командой как ps | grep "^$$ "
. (Это grep
regex однозначно определит PID, таким образом, не будет никаких ложных положительных сторон.
По умолчанию rsync сравнивает только метаданные файлов. это означает отметку времени, размер и атрибуты. среди прочего. но не содержимое файлов.
rsync -n -a -i --delete source/ target/
объяснение:
-n
фактически не копировать и не удалять <- ЭТО ВАЖНО !! 1 -a
сравнить все метаданные файла, такие как временная метка и атрибуты -i
выводит по одной строке информации для каждого файла - удаление
также сообщает о файлах, которых нет в исходном примечании: важно добавлять имена каталогов с помощью косой черты. это вещь rsync.
, если вы также хотите видеть строки, напечатанные для идентичных файлов, предоставьте -i
дважды
rsync -n -a -ii --delete source/ target/
пример вывода:
*deleting removedfile (file in target but not in source)
.d..t...... ./ (directory with different timestamp)
>f.st...... modifiedfile (file with different size and timestamp)
>f+++++++++ newfile (file in source but not in target)
.f samefile (file that has same metadata. only with -ii)
помните, что rsync сравнивает только метаданные. это означает, что если содержимое файла изменилось, но метаданные остались прежними, rsync сообщит, что файл такой же. это маловероятный сценарий. так что либо верьте, что, когда метаданные одинаковы, тогда данные такие же, либо вам придется сравнивать данные файла побитно.
бонус: информацию о ходе выполнения см. Здесь: Оценить время или работу, оставшуюся до завершения для rsync?
Используйте -q
(--brief
) опция с diff -r
(diff -qr
). От info
страница для GNU diff
:
1.6 Суммирование, какие файлы отличаются
Когда Вы только хотите узнать, отличаются ли файлы, и Вы не заботитесь, каковы различия, можно использовать сводный выходной формат. В этом формате, вместо того, чтобы показать различия между файлами,
diff' simply reports whether files differ. The
- резюме' ('-q') опция выбирает этот выходной формат.Этот формат особенно полезен при сравнении содержания двух каталогов. Это также намного быстрее, чем выполнение нормали с методической точностью сравнения, потому что 'разность' может прекратить анализировать файлы, как только это знает, что существуют любые различия.
Это не выдержит сравнение линию за линией, а скорее файл в целом, который значительно ускоряет процессор (что' Вы ищете).
Вот быстрый сценарий Python, который проверит, что имена файлов, mtimes, и размеры файла являются всеми одинаковыми:
import os
import sys
def getStats(path):
for pathname, dirnames, filenames in os.walk(path):
for filename in ( os.path.join(pathname, x) for x in filenames ):
stat = os.stat(filename)
yield filename[len(path):], stat.st_mtime, stat.st_size
sys.exit(tuple(getStats(sys.argv[1])) != tuple(getStats(sys.argv[2])))
Если требуется сравнить только структуру и некоторую основную информацию о файлах, можно попробовать что-то вроде этого:
diff <(cd $DIR1 && ls -laR) <(cd $DIR2 && ls -laR)
Я не протестировал его, таким образом, любые редактирования приветствуются :)
<()
имеет его собственную среду. Отредактированный.
– a CVn
11.12.2016, 14:43
Основанный на сценарии Криса Дауна, этот сценарий немного более "нагляден". Вызывая его с двумя аргументами папка1
и папка2
, он обходит первую папку и для каждого файла ищет соответствующий файл во второй папке. Если он найден, относительный путь печатается зеленым цветом, если у них другое время изменения или размер, он печатается желтым цветом, а если он не найден, он печатается красным.
#!/usr/bin/env python
import os
import sys
from termcolor import colored
def compare_filestats(file1,file2):
"""
Compares modified time and size between two files.
Return:
-1 if file1 or file2 does not exist
0 if they exist and compare equal
1 if they have different modified time, but same size
2 if they have different size, but same modified time
3 if they have different size, and different modified time
"""
if not os.path.exists(file1) or not os.path.exists(file2):
return -1
stat1 = os.stat(file1)
stat2 = os.stat(file2)
return (stat1.st_mtime != stat2.st_mtime) \
+ 2*(stat1.st_size != stat2.st_size)
def compare_folders(folder1,folder2):
"""
folder1: serves as reference and will be walked through
folder2: serves as target and will be querried for each file in folder1
Prints colored status for each file in folder1:
missing: file was not found in folder2
mtime : modified time is different
size : filesize is different
ok : found with same filestats
"""
for dirpath, dirnames, filenames in os.walk(folder1):
for file1 in ( os.path.join(dirpath, x) for x in filenames ):
relpath = file1[len(folder1):]
file2 = os.path.join( folder2, relpath )
comp = compare_filestats(file1,file2)
if comp < 0:
status = colored('[missing]','red')
elif comp == 1:
status = colored('[mtime ]','yellow')
elif comp >= 2:
status = colored('[size ]','yellow')
else:
status = colored('[ok ]','green')
print status, relpath
if __name__ == '__main__':
compare_folders(sys.argv[1],sys.argv[2])
Обратите внимание, что этого недостаточно , чтобы решить, являются ли две папки одинаковыми, вам нужно будет запустить его обоими способами, чтобы убедиться. На практике, если вы просто хотите узнать , совпадают ли папки , то сценарий Криса лучше.Если вы хотите знать , что отсутствует или отличается от одной папки к другой , то мой сценарий скажет вам.
ПРИМЕЧАНИЕ: вам потребуется установить termcolor, pip install termcolor
.
Если вам нужно только узнать, отличаются ли файлы из двух веток файловой системы (без просмотра внутренних файлов ), вы можете сделать что-то вроде этого:
find /opt/branch1 -type f | sort | xargs -i md5sum {} >/tmp/branch1;
find /opt/branch2 -type f | sort | xargs -i md5sum {} >/tmp/branch2;
diff /tmp/branch1 /tmp/branch2;
ХТХ
Сохранить следующий код как: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
# 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://$((2048*sizeInMB))) 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
Как использовать:
Синтаксис:<caller_shell> '/path/to/diffm.sh' <dir_tree1> <dir_tree2>
где:<caller_shell>
может быть любой оболочкой :dash
, bash
, zsh
или любой другой оболочкой, совместимой с синтаксисом оболочки dash
.
Что он делает :Сравнивает файлы (рекурсивно )в двух предоставленных путях дерева каталогов(<dir_tree1>
и<dir_tree2>
)по:
Примечания:
Этот сценарий сравнивает только файлы, а не каталоги в <dir_tree1>
и <dir_tree2>
.
Чтобы узнать, как использовать скрипт, вы можете вызвать его с флагом --help
.
Midnight Commander (mc в Debian linux )— это файловый менеджер с двумя -панелями -с функцией сравнения каталогов (C -x d; то есть, удерживая клавишу управления, нажмите x, отпустите, затем нажмите d ). У него есть три варианта :«Быстрый», «Только размер» и «Тщательный». Quick или Size only могут быть вам полезны. Затем он выделяет файлы, которые различаются на обеих панелях, поэтому вы можете что-то сделать с одним или обоими наборами.
Я использую описанный выше метод rsync, когда у меня много файлов и ясный исходный и целевой каталоги. Но я часто обнаруживаю, что у меня есть два каталога, в каждом из которых есть новые файлы для синхронизации, и в итоге я часто использую mc.
Я только что обнаружил tree
.
tree old_dir/ > tree_old
tree new_dir/ > tree_new
vimdiff tree_old tree_new