cómo usar patch y diff para fusionar dos archivos y resolver conflictos automáticamente

19

He leído sobre diff y patch pero no puedo entender cómo aplicar lo que necesito. Supongo que es bastante simple, así que para mostrar mi problema, tome estos dos archivos:

a.xml

<resources>
   <color name="same_in_b">#AAABBB</color>
   <color name="not_in_b">#AAAAAA</color>
   <color name="in_b_but_different_val">#AAAAAA</color>
   <color name="not_in_b_too">#AAAAAA</color>
</resources>

b.xml

<resources>
   <color name="same_in_b">#AAABBB</color>
   <color name="in_b_but_different_val">#BBBBBB</color>
   <color name="not_in_a">#AAAAAA</color>
</resources>

Quiero tener una salida, que se ve así (el orden no importa):

<resources>
   <color name="same_in_b">#AAABBB</color>
   <color name="not_in_b">#AAAAAA</color>
   <color name="in_b_but_different_val">#BBBBBB</color>
   <color name="not_in_b_too">#AAAAAA</color>
   <color name="not_in_a">#AAAAAA</color>
</resources>

La fusión debe contener todas las líneas a lo largo de estas reglas simples:

  1. cualquier línea que solo esté en uno de los archivos
  2. Si una línea tiene la misma etiqueta de nombre pero un valor diferente, tome el valor de la segunda

Quiero aplicar esta tarea dentro de un script bash, por lo que no necesariamente debe hacerse con diff y patch, si otro programa es mejor

Rafael T
fuente
diffpuede decirle qué líneas están en un archivo pero no en el otro, sino solo en la granularidad de líneas enteras. patchsolo es adecuado para realizar los mismos cambios en un archivo similar (tal vez una versión diferente del mismo archivo o un archivo completamente diferente donde, sin embargo, los números de línea y las líneas circundantes para cada cambio son idénticos a su archivo original). Entonces no, no son particularmente adecuados para esta tarea. Es posible que desee echar un vistazo, wdiffpero la solución probablemente requiera un script personalizado. Dado que sus datos se parecen a XML, es posible que desee buscar alguna herramienta XSL.
tripleee
1
¿Por qué todas las respuestas con guiones personalizados? La fusión es un problema estándar y complejo, y existen buenas herramientas para ello. No reinventes la rueda.
alexis

Respuestas:

23

No necesitas patchpara esto; es para extraer cambios y enviarlos sin la parte inalterada del archivo.

La herramienta para fusionar dos versiones de un archivo es merge, pero tal como se @vonbrandescribió, necesita el archivo "base" del cual sus dos versiones divergieron. Para hacer una fusión sin ella, use diffasí:

diff -DVERSION1 file1.xml file2.xml > merged.xml

Incluirá cada conjunto de cambios en los comandos de estilo C #ifdef/ #ifndef"preprocesador", como este:

#ifdef VERSION1
<stuff added to file1.xml>
#endif
...
#ifndef VERSION1
<stuff added to file2.xml>
#endif

Si una línea o región difiere entre los dos archivos, obtendrá un "conflicto", que se ve así:

#ifndef VERSION1
<version 1>
#else /* VERSION1 */
<version 2>
#endif /* VERSION1 */

Así que guarde el resultado en un archivo y ábralo en un editor. Busque cualquier lugar donde #elseaparezca y resuélvalos manualmente. Luego guarde el archivo y ejecútelo grep -vpara deshacerse del resto #if(n)defy las #endiflíneas:

grep -v '^#if' merged.xml | grep -v '^#endif' > clean.xml

En el futuro, guarde la versión original del archivo. mergepuede darle mejores resultados con la ayuda de la información adicional. (Pero tenga cuidado: mergeedita uno de los archivos en el lugar, a menos que lo use -p. Lea el manual).

alexis
fuente
sed -e "s/^#else.*$/\/\/ conflict/g"
Agregué
1
No creo que sea una buena idea. Como escribí en mi respuesta, debería eliminar las #elselíneas manualmente, en el editor durante la resolución de conflictos.
alexis
6

merge(1) probablemente esté más cerca de lo que desea, pero eso requiere un antepasado común para sus dos archivos.

Una forma (¡sucia!) De hacerlo es:

  1. Deshágase de la primera y última línea, use grep(1)para excluirlas
  2. Aplastar los resultados juntos
  3. sort -u deja una lista ordenada, elimina duplicados
  4. Reemplazar primera / última línea

Humm ... algo en la línea:

echo '<resources>'; grep -v resources file1 file2 | sort -u; echo '</resources>'

podría hacer.

vonbrand
fuente
funciona en este ejemplo en particular, pero NO en general: si name in_b_but_different_valtiene un valor de #00AABBclasificación, lo colocará en la parte superior y borrará el segundo valor en lugar del primero
Rafael T
para la solución óptima en este caso, tendría que analizar el XML, con un analizador XML real, no los hacks anteriores, y producir una nueva salida XML combinada a partir de eso. diff / patch / sort etc. son solo hacks diseñados para "ejemplos particulares", para una solución general son simplemente las herramientas incorrectas
frostschutz 02 de
@alzheimer, prepara algo simple para mostrarnos ...
vonbrand
Aparentemente diff3funciona de la misma manera. Requerir un archivo ancestro común. ¿Por qué no hay una herramienta CLI simple que solo combine 2 archivos en función de lo que se diffmuestra?
CMCDragonkai
5

sdiff (1) - fusión lado a lado de diferencias de archivo

Use la --outputopción, esto fusionará interactivamente cualquiera de los dos archivos. Utiliza comandos simples para seleccionar un cambio o editar un cambio.

Debe asegurarse de que la EDITORvariable de entorno esté establecida. El editor predeterminado para comandos como "eb" suele ser edun editor de línea .

EDITOR=nano sdiff -o merged.txt file1.txt file2.txt
Cody Allan Taylor
fuente
1
Me parece mejor usarlo vimcomo EDITOR. Pero esta es la mejor solución, ¡también viene con el diffcomando!
CMCDragonkai
1

Aquí una solución simple que funciona fusionando hasta 10 archivos :

#!/bin/bash

strip(){
    i=0
    for f; do
        sed -r '
            /<\/?resources>/ d
            s/>/>'$((i++))'/
        ' "$f"
    done
}

strip "$@" | sort -u -k1,1 -t'>' | sed '
    1 s|^|<resources>\n|
    s/>[0-9]/>/
    $ a </resources>
'

tenga en cuenta que el argumento que viene primero tiene prioridad, por lo que debe llamar:

script b.xml a.xml

para mantener valores comunes guardados en b.xmllugar de a.xml.

script b.xml a.xml salidas:

<resources>
   <color name="in_b_but_different_val">#BBBBBB</color>
   <color name="not_in_a">#AAAAAA</color>
   <color name="not_in_b">#AAAAAA</color>
   <color name="not_in_b_too">#AAAAAA</color>
   <color name="same_in_b">#AAABBB</color>
</resources>
neurino
fuente
1

Otro truco horrible, podría simplificarse, pero: P

#!/bin/bash

i=0

while read line
do
    if [ "${line:0:13}" == '<color name="' ]
    then
        a_keys[$i]="${line:13}"
        a_keys[$i]="${a_keys[$i]%%\"*}"
        a_values[$i]="$line"
        i=$((i+1))
    fi
done < a.xml

i=0

while read line
do
    if [ "${line:0:13}" == '<color name="' ]
    then
        b_keys[$i]="${line:13}"
        b_keys[$i]="${b_keys[$i]%%\"*}"
        b_values[$i]="$line"
        i=$((i+1))
    fi
done < b.xml

echo "<resources>"

i=0

for akey in "${a_keys[@]}"
do
    print=1

    for bkey in "${b_keys[@]}"
    do
        if [ "$akey" == "$bkey" ]
        then
            print=0
            break
        fi
    done

    if [ $print == 1 ]
    then
        echo "  ${a_values[$i]}"
    fi

    i=$(($i+1))
done

for value in "${b_values[@]}"
do
    echo "  $value"
done

echo "</resources>"
Frostschutz
fuente
0

OK, segundo intento, ahora en Perl (¡ no calidad de producción, sin verificación!):

#!/usr/bin/perl

open(A, "a.xml");

while(<A>) {
  next if(m;^\<resource\>$;);
  next if(m;^\<\/resource\>$;);
  ($name, $value) = m;^\s*\<color\s+name\s*\=\s*\"([^"]+)\"\>([^<]+)\<\/color\>$;;
  $nv{$name} = $value if $name;
}

close(A);

open(B, "b.xml");

while(<B>) {
  next if(m;^\<resource\>$;);
  next if(m;^\<\/resource\>$;);
  ($name, $value) = m;^\s*\<color\s+name\s*\=\*\"([^"]+)\"\>([^<]+)\<\/color\>$;;
  $nv{$name} = $value if $name;
}

close(B);

print "<resource>\n";
foreach (keys(%nv)) {
    print "   <color name=\"$_\">$nv{$_}</color>\n";
}
print "</resource>\n";
vonbrand
fuente
0

Otro, usando cut and grep ... (toma a.xml b.xml como argumentos)

#!/bin/bash

zap='"('"`grep '<color' "$2" | cut -d '"' -f 2 | tr '\n' '|'`"'")'
echo "<resources>"
grep '<color' "$1" | grep -E -v "$zap"
grep '<color' "$2"
echo "</resources>"
Frostschutz
fuente
echoes la acción predeterminada, por lo que xargs echoes superflua. ¿Por qué no simplemente de tr '\n' '|'todos modos?
tripleee
Buen punto: es solo un truco rápido. Lo editaré
frostschutz