# [HOWTO] Using dispatch-conf with GUI diff/merge tools

## Guenther Brunthaler

Hi all,

I have been using the console-based dispatch-conf for quite a while now.

But there are so many beautiful GUI tools out there like kdiff3, kompare or xxdiff which can be used for showing differences or merging - so why not using them in concert with dispatch-conf?

On the other hand, there are situations when I actually want to use a console-based tool - such as when logged into a remote account via a low-bandwidth SSH-session.

Any enhancement to dispatch-conf should therefore support both approaches.

In order to achieve this goal, I wrote two scripts which can be set as the "diff" and "merge" tool in /etc/dispatch-conf.conf and will allow to optionally use a GUI-based tool for diffing/merging when running dispatch-conf.

The scripts always ask before running a GUI tool and remember the last answer as a default for the next prompt.

It the user decides not to run the GUI tools, the usual console-based diff and sdiff tools will be run. That is, my scripts are intended to be as backwards-compatible as possible with the traditional behavior of dispatch-conf.

The scripts also try to be smart and probe for the presence of a $DISPLAY variable - if it is not set, they always use the console-based diff/merge tools and do not prompt the user.

The scripts also probe whether a GUI tool is installed before using it. If no GUI tool can be found then the scripts also fall back to the console tools without unnecessary prompts.

The current version of the scripts directly support the following GUI tools for comparison out of the box, and if more than one of those tools is installed they use the following order of preference:

kompare

kdiff3

xxdiff

If none of those tools is installed, the console-based "diff -u" is used as a fallback.

The current version of the scripts directly support the following GUI tools for merging out of the box, and if more than one of those tools is installed they use the following order of preference:

kdiff3

xxdiff

If none of those tools is installed, the console-based "sdiff" is used as a fallback.

The above list of comparison/merging tools can easily be extended and the order of preference can easily be customized by modifying the script functions gui_showdiff(), console_showdiff(), gui_merge() and console_merge() as desired.

Once the scripts have been installed, dispatch-conf works in the following way:

When comparing files, the first diff for a new configuration file is always done in console mode. This is because starting a GUI tool might involve considerable overhead, and for small changes it is often not worth it.

When the user does not select any specific action at the dispatch-conf prompt after the initial console prompt of dispatch-conf but rather presses the Enter key (which instructs dispatch-conf to display the diff again), the user will be prompted whether to run the GUI-based diff tool or rather again the console-based diff tool.

When merging files, the user will immediately be prompted whether to run the GUI-based merge tool or rather the console-based merge tool.

The prompts will not occur if X11 is not running (no $DISPLAY variable set) or if no GUI tools can be found.

Last edited by Guenther Brunthaler on Thu Aug 16, 2007 9:59 am; edited 1 time in total

----------

## Guenther Brunthaler

Copy the scripts to the /usr/local/libexec directory

Modify your /etc/dispatch-conf.conf as indicated in the comments at the beginning of both scripts.

In order to make it short, this is what should go into /etc/dispatch-conf.conf:

```

# Diff for display

# %s old file

# %s new file

diff="/usr/local/libexec/dispatch-conf-diff %s %s"

# Tool for interactive merges.

# %s output file

# %s old file

# %s new file

merge="/usr/local/libexec/dispatch-conf-merge %s %s %s"

```

----------

## Guenther Brunthaler

This script will be used for comparison.

LAST UPDATE:

Revision: 901

Wed Aug 15 17:12:22 UTC 2007

Save it as "/usr/local/libexec/dispatch-conf-diff":

```

#! /bin/sh

# Combined gui/textmode diff for dispatch-conf

#

# Customize functions gui_showdiff and console_showdiff as desired.

#

# This script should have been saved

# as "/usr/local/libexec/dispatch-conf-diff".

#

# In order to make dispatch-conf use this script, replace

# the "diff"-setting in /etc/dispatch-conf.conf by this:

#

# > # Diff for display

# > # %s old file

# > # %s new file

# > diff="/usr/local/libexec/dispatch-conf-diff %s %s"

#

# Note: You have to remove the "# > "-prefix from each line

# after copying the above lines to /dispatch-conf.conf!

#

# Basic idea: The first time, always a command line diff is shown.

# If the diff operation is repeated for the very same file,

# the user is prompted whether to invoke a graphical tool.

# This prompt is only done if $DISPLAY has been set.

#

# $HeadURL: /caches/xsvn/uxadm/trunk/usr/local/libexec/dispatch-conf-diff $

# $Author: root $

# $Date: 2007-08-15T17:11:03.452918Z $

# $Revision: 901 $

#

# Written in 2007 by Guenther Brunthaler

OLD="$1"; NEW="$2"

STATE_DIR=/var/run/dispatch-conf

gui_showdiff() {

   if have kompare; then

      rungui kompare "$OLD" "$NEW"

   elif have kdiff3; then

      rungui kdiff3 "$OLD" "$NEW"

   elif have xxdiff; then

      rungui xxdiff "$OLD" "$NEW"

   else

      warn "No GUI tool has been installed" \

         "- falling back to console tool."

      console_showdiff

   fi

}

console_showdiff() {

   diff -Nu "$OLD" "$NEW" | run less --no-init --QUIT-AT-EOF

}

# Check whether we have tool $1 in path.

have() {

   which "$1" > /dev/null 2>& 1

}

die() {

   echo "ERROR: $*" >& 2

   exit 1

}

warn() {

   echo "WARNING: $*" >& 2

}

run() {

   "$@" && return

   die "Command >>>$*<<< failed!"

}

# Run command hiding its output

# and ignoring its return value.

rungui() {

   "$@" > /dev/null 2>& 1

}

# $1: variable name

# $2: value

varset() {

   eval "$1=$2"

}

# $1: file name

# $2: variable name

# $3: default

load_value() {

   local fn

   fn="$STATE_DIR/$1"

   varset "$2" "$3"

   test -f "$fn" && test -r "$fn" || return 0

   run read "$2" < "$fn"

}

load_state() {

   WANTED_GUI=

   test "x$DISPLAY" != x && WANTED_GUI=y

   load_value last_md5 LAST_MD5 N/A

   load_value diff_gui WANTED_GUI "$WANTED_GUI"

   CHANGED=

}

# $1: file name

# $2: value

save_value() {

   local fn

   fn="$STATE_DIR/$1"

   test -e "$fn" && run rm -f "$fn"

   run printf "%s\n" "$2" > "$fn"

}

save_state() {

   test -z "$CHANGED" && return

   test -d "$STATE_DIR" || run mkdir -m 700 "$STATE_DIR"

   save_value diff_gui "$WANTED_GUI"

   save_value last_md5 "$LAST_MD5"

}

check_file() {

   test -f "$1" && test -r "$1" && return

   die "Cannot read file '$1'!"

}

# $1: result variable

# $2 ...: files

checksum_files() {

   local var cs

   var=$1; shift

   cs=empty

   while [ $# -gt 0 ]; do

      check_file "$1"

      cs=$(

         {

            echo "$cs"

            run cat "$1"

         } | run md5sum -b | cut -c 1-32

      )

      shift

   done

   varset $var $cs

}

# $1: result variable

# $2: locale keyword to get

# $3: default if not available

locale_lookup() {

   local val

   val="$(locale "$2")"

   test "x$val" = x && val=$3

   varset $1 "$val"

}

# $1: Regex

# $2: candidate string

check_re() {

   test "$(printf "%s\n" "$2" | grep "$1" | wc -l)" = 1

}

# $1: result variable

# $2: prompt

# $3: "test -n" boolean default

yesno() {

   local preset resp ye ne

   preset="yes/No"

   test -n "$3" && preset="Yes/no"

   ye="$(locale yesexpr)"

   locale_lookup ye yesexpr "[^Yy].*"

   locale_lookup ne noexpr "[^Nn].*"

   while true; do

      read -p "$2 [$preset]? " resp || resp=

      if [ -z "$resp" ]; then

         resp=$3

      elif check_re "$ye" "$resp"; then

         resp=y

      elif check_re "$ne" "$resp"; then

         resp=

      else

         echo "Invalid answer."

         continue

      fi

      break

   done

   varset $1 $resp

}

showdiff_different() {

   local md5

   checksum_files md5 "$OLD" "$NEW"

   load_state

   if [ "$LAST_MD5" = "$md5" ]; then

      if [ "x$DISPLAY" != x ]; then

         local wg

         yesno wg "Compare files using GUI tools" \

            "$WANTED_GUI"

         if [ "x$wg" != "$WANTED_GUI" ]; then

            CHANGED=y

            WANTED_GUI=$wg

         fi

         if [ -n "$WANTED_GUI" ]; then

            gui_showdiff || return 0

         else

            console_showdiff || return 0

         fi

      else

         console_showdiff || return 0

      fi

   else

      CHANGED=y

      LAST_MD5=$md5

      console_showdiff || return 0

   fi

   save_state

}

if [ $# != 2 ]; then

   die "Usage: ${0##*/} <oldfile> <newfile>"

fi

check_file "$OLD"

check_file "$NEW"

if cmp -s "$OLD" "$NEW"; then

   console_showdiff

else

   showdiff_different

fi

```

Last edited by Guenther Brunthaler on Wed Aug 15, 2007 5:14 pm; edited 1 time in total

----------

## Guenther Brunthaler

This script will be used for merging.

LAST UPDATE:

Revision: 901

Wed Aug 15 17:12:22 UTC 2007

Save it as "/usr/local/libexec/dispatch-conf-merge":

```

#! /bin/sh

# Combined gui/textmode merge for dispatch-conf

#

# Customize functions gui_merge and console_merge as desired.

#

# This script should have been saved

# as "/usr/local/libexec/dispatch-conf-merge".

#

# In order to make dispatch-conf use this script, replace

# the "merge"-setting in /etc/dispatch-conf.conf by this:

#

# > # Tool for interactive merges.

# > # %s output file

# > # %s old file

# > # %s new file

# > merge="/usr/local/libexec/dispatch-conf-merge %s %s %s"

#

# Note: You have to remove the "# > "-prefix from each line

# after copying the above lines to /dispatch-conf.conf!

#

# $HeadURL: /caches/xsvn/uxadm/trunk/usr/local/libexec/dispatch-conf-merge $

# $Author: root $

# $Date: 2007-08-15T17:11:03.452918Z $

# $Revision: 901 $

#

# Written in 2007 by Guenther Brunthaler

OUT="$1"; OLD="$2"; NEW="$3"

STATE_DIR=/var/run/dispatch-conf

gui_merge() {

   if have kdiff3; then

      rungui kdiff3 --merge --output "$OUT" "$OLD" "$NEW"

   elif have xxdiff; then

      rungui xxdiff --merged-filename "$OUT" "$OLD" "$NEW"

   else

      warn "No GUI tool has been installed" \

         "- falling back to console tool."

      console_merge

   fi

}

console_merge() {

   sdiff --suppress-common-lines --output="$OUT" "$OLD" "$NEW"

}

# Check whether we have tool $1 in path.

have() {

   which "$1" > /dev/null 2>& 1

}

die() {

   echo "ERROR: $*" >& 2

   exit 1

}

warn() {

   echo "WARNING: $*" >& 2

}

run() {

   "$@" && return

   die "Command >>>$*<<< failed!"

}

# Run command hiding its output

# and ignoring its return value.

rungui() {

   "$@" > /dev/null 2>& 1

}

# $1: variable name

# $2: value

varset() {

   eval "$1=$2"

}

# $1: file name

# $2: variable name

# $3: default

load_value() {

   local fn

   fn="$STATE_DIR/$1"

   varset "$2" "$3"

   test -f "$fn" && test -r "$fn" || return 0

   run read "$2" < "$fn"

}

load_state() {

   WANTED_GUI=

   test "x$DISPLAY" != x && WANTED_GUI=y

   load_value merge_gui WANTED_GUI "$WANTED_GUI"

   CHANGED=

}

# $1: file name

# $2: value

save_value() {

   local fn

   fn="$STATE_DIR/$1"

   test -e "$fn" && run rm -f "$fn"

   run printf "%s\n" "$2" > "$fn"

}

save_state() {

   test -z "$CHANGED" && return

   test -d "$STATE_DIR" || run mkdir -m 700 "$STATE_DIR"

   save_value merge_gui "$WANTED_GUI"

}

check_file() {

   test -f "$1" && test -r "$1" && return

   die "Cannot read file '$1'!"

}

# $1: result variable

# $2: locale keyword to get

# $3: default if not available

locale_lookup() {

   local val

   val="$(locale "$2")"

   test "x$val" = x && val=$3

   varset $1 "$val"

}

# $1: Regex

# $2: candidate string

check_re() {

   test "$(printf "%s\n" "$2" | grep "$1" | wc -l)" = 1

}

# $1: result variable

# $2: prompt

# $3: "test -n" boolean default

yesno() {

   local preset resp ye ne

   preset="yes/No"

   test -n "$3" && preset="Yes/no"

   ye="$(locale yesexpr)"

   locale_lookup ye yesexpr "[^Yy].*"

   locale_lookup ne noexpr "[^Nn].*"

   while true; do

      read -p "$2 [$preset]? " resp || resp=

      if [ -z "$resp" ]; then

         resp=$3

      elif check_re "$ye" "$resp"; then

         resp=y

      elif check_re "$ne" "$resp"; then

         resp=

      else

         echo "Invalid answer."

         continue

      fi

      break

   done

   varset $1 $resp

}

merge_different() {

   load_state

   if [ "x$DISPLAY" != x ]; then

      local wg

      yesno wg "Merge files using GUI tool" \

         "$WANTED_GUI"

      if [ "x$wg" != "$WANTED_GUI" ]; then

         CHANGED=y

         WANTED_GUI=$wg

      fi

      if [ -n "$WANTED_GUI" ]; then

         gui_merge || return 0

      else

         console_merge || return 0

      fi

   else

      console_merge || return 0

   fi

   save_state

}

if [ $# != 3 ]; then

   die "Usage: ${0##*/} <merged_outfile> <oldfile> <newfile>"

fi

check_file "$OLD"

check_file "$NEW"

if cmp -s "$OLD" "$NEW"; then

   console_merge

else

   merge_different

fi

```

Last edited by Guenther Brunthaler on Wed Aug 15, 2007 5:15 pm; edited 1 time in total

----------

## Guenther Brunthaler

If the scripts don't seem to work, please first verify their MD5 checksums before reporting a bug!

LAST UPDATE:

Revision: 901

Wed Aug 15 17:12:22 UTC 2007

```

ee85d251960cc8febab1701546e5a558  dispatch-conf-diff

d96e29d897ce351d8cde3a56e0eda60f  dispatch-conf-merge

```

It is all too easy to make a mistake when copying/pasting a script from a forum article.

Update: It seems the indentation of my scripts has been screwed up by pasting it into the article! All the lines should be indented by tabs and not by spaces!

So, before trying to verify the MD5 checksums, please replace all leading spaces into tabs! (4 spaces = 1 tab.)Last edited by Guenther Brunthaler on Wed Aug 15, 2007 5:19 pm; edited 3 times in total

----------

## pilla

Moved from Portage & Programming to Documentation, Tips & Tricks.

----------

## steveL

Hi Guenther,

  I haven't tried this out yet, as I am snowed under and about to crash out. (I also use etc-proposals, not cfg-update or dispatch-conf.) I was wondering would you think about how we could maybe add this to update? If it's useful for you I am sure it'd be useful to others.

  One quick point: I noticed you use which; in bash type -P is quicker (as it's built-in, which is an external which means a fork.) The -P forces path lookup, but doesn't always have to be done; I am a bit hazy on when and why if I'm honest. We were using hash -t blah 2>/dev/null before in update, but it was falling over when we were coding; might be to do with the fact that the script was being edited and restarted/stopped all the time, but it was enough for us to stop using it and fallback to path lookup, and we were a bit pushed for time so I haven't got the canonical answer (although hash would be the preferred option aiui.)

help type or help hash in a terminal will give more info (try help test, that's a great one ;)

Regards,

steveL.

----------

## Guenther Brunthaler

Hi Steve,

 *steveL wrote:*   

> I was wondering would you think about how we could maybe add this to update? If it's useful for you I am sure it'd be useful to others.

 

Sure! If you think you could reuse part of my script for enhancing update even further, feel free to take from it what you consider useful!

I'm always happy if my work can help someone else - that's why I have been posting it.

 *steveL wrote:*   

> 
> 
> I noticed you use which; in bash type -P is quicker
> 
> 

 

Yes, you are right.

But I generally try to avoid bash-specific commands as much as I can, because I want my scripts to be compatible with the standard UNIX sh as much as possible.

I have read autoconf's "Writing Portable Shell Scripts" and always try to adhere mostly to it.

Obviously, I'm not doing this all of the time, because otherwise I needed to use m4 instead of shell functions... but that would feel a bit extreme even to me!  :Wink: 

So, in practice, I try to least achieve compatibility with the POSIX ksh - or with Busybox' ash - whenever I can.

BTW, you might also have noticed that I am using grep for evaluating regular expressions rather than using bash's builtin regex operator - this was done for the very same reason.

Another reason why I did not even try any speed optimization for my script is that it is not time-critical.

It's not a number crunching batch-job, and neither it is a realtime-application.

If one considers the time it will take to actually launch a GUI tool, anything else will be neglectible in comparison.

Furthermore, the real work in my script is done by external tools like md5sum, cmp and diff - those will use up most of the script's actual running time, leaving little running time left to be tuned for efficiency within the script itself.

Anyway, if you decide to pick out sections of my script for putting it into update, feel free to replace any constructs aiming for portability by more efficient bash-builtins: I certainly do understand that a script should be written either with efficiency or portability in mind, and that mixing both concepts in a single script makes no sense.

----------

## steveL

 *Guenther Brunthaler wrote:*   

> Sure! If you think you could reuse part of my script for enhancing update even further, feel free to take from it what you consider useful!
> 
> I'm always happy if my work can help someone else - that's why I have been posting it.

 

Cool! We're doing some more on it later today so I'll have a look at it then.

 *Quote:*   

> But I generally try to avoid bash-specific commands as much as I can, because I want my scripts to be compatible with the standard UNIX sh as much as possible.

 

Ugh my bad, I didn't notice the #!/bin/sh at the top. In that mode bash won't allow half the stuff I like using ;) It's amazing how many people refuse to believe that ("But /bin/sh is a link to /bin/bash!") to the extent that I added !sh to #bash greybot (I just got so tired of arguing the point.)

 *Quote:*   

>  I certainly do understand that a script should be written either with efficiency or portability in mind, and that mixing both concepts in a single script makes no sense.

 

Yeah I never thought of it like that: good point. I just really like bash; you'd enjoy talking to greycat in #bash as he's always banging on about portability; more wrt commands like sed and find, or GNU extensions which won't work everywhere, than bash vs sh, but he's quite happy to talk about making shell-scripts portable. Thing is he has bash on linux, BSD and some old HP-UX from 1995. Personally I think requiring bash is ok since it's so prevalent, can be installed on just about anything (not too good for embedded I agree, but that's kinda specialist and if you're doing serious embedded stuff, you're mostly in C and you can afford to rewrite stuff) and has an excellent man page (unlike most GNU utils.) I know others feel differently, I am not trying to get into an argument ;)

The stuff you were saying about D was quite interesting too; I wish you'd join us in #friendly-coders sometime (irc.freenode.org) as it'd be cool to talk code and language design; my nick is igli btw.

----------

## Guenther Brunthaler

Hi Steve,

 *steveL wrote:*   

> I wish you'd join us in #friendly-coders sometime (irc.freenode.org) as it'd be cool to talk code and language design

 

As much as I honestly appreciate your invitation, I'm afraid I'm not an IRC type of guy.

I'm a rather slow typer, so I prefer non-realtime means of communication where this does not matter.

Also there are often time-related constraints. For instance my time zone is 8 to 10 hours ahead of US time zones, which means there is only a rather small time window when IRC meetings are easily possible to be attended by all.

Nevertheless, thank your for your appreciation, which I mutually return to the fullest!

----------

## steveL

 *Guenther Brunthaler wrote:*   

> As much as I honestly appreciate your invitation, I'm afraid I'm not an IRC type of guy.
> 
> I'm a rather slow typer, so I prefer non-realtime means of communication where this does not matter.
> 
> Also there are often time-related constraints. For instance my time zone is 8 to 10 hours ahead of US time zones, which means there is only a rather small time window when IRC meetings are easily possible to be attended by all.

 

Ah well no matter, although I would say I am in the UK so only an hour difference, and also that irc is fun in the sense that it can be asynchronous, since you can always see what has been typed. If someone types slowly it doesn't matter too much, although some of the younger types make a point of typing really quickly. Thankfully I am too old for all that now. But no bother, quite happy to collaborate via forums or email.

----------

