As much as I like the Linux command line, there are times when it’s easier to use a point-and-click interface. One such example is when I need to compare two files in different directories. Typing out the full paths on the console can be tedious whereas it would be much simpler to just drag’n’drop two files from a Nautilus window onto a comparison tool. Unfortunately I didn’t know of any such tool – so I wrote one myself.
The architecture
When I first thought about creating a drag’n’drop comparator tool, I feared that I had to create a full-fledged GTK application or something similar. That would have been a lot of effort when all I needed was a GUI wrapper for a command-line operation like cmp /path/to/fileA /path/to/fileB
.
Thankfully there is an easier way. All that’s needed for a program launcher is a “desktop entry” or .desktop file according to specification. It’s a text file containing information like the application’s name, icon, and what command to run when the tool is launched. The latter may include field codes like %F which evaluates to a list of filenames for those items drag’n’dropped onto the launcher.
Now that handling drag’n’drop turned out to be easy, the whole tool could be finished just by adding the line Exec=cmp %F
to the .desktop file, right? Well, unfortunately it’s not that simple as the result would be a terminal window that pops up and immediately gets closed once the comparison is done. Putting aside that this would be a pretty ugly solution, it also doesn’t contain any kind of error handling. What we need is an additional shell script that handles the program logic and uses Zenity for user interaction. While cmp
is still used at the core of the script, its return code is evaluated to present the user with dialogs native to their desktop environment.
Another main part of the script is the evaluation of its input parameters. The user is expected to drag’n’drop two files, but what if they picked a different number of files or none at all? Thanks to Zenity a nice error message will be shown, but there’s one more special case to be handled. As I wrote in the introduction, the main motivation in writing this tool has been to compare two files from different directories via drag’n’drop. Unfortunately it’s impossible to select two files from different directories (i.e., different Nautils windows) at the same time. Therefore it’s necessary to include a way to drag’n’drop two files consecutively. Whenever the script recognizes that only one argument has been passed, it assumes that this scenario is in play and consequently presents the user with a file selection dialog. The user can now navigate to the second file or drag’n’drop it from e.g. a Nautilus window onto the dialog. It took me a while to figure out that the latter requires the file to be dropped somewhere in the file list part of the dialog which then automatically changes to the directory containing the dropped file. Click OK after that and the comparison takes place. If you’re using a non-GNOME system, your mileage may vary.
The code
[Desktop Entry] Version=1.0 Type=Application Name=Drag'n'drop comparator Comment=Compares two files drag'n'dropped onto it TryExec=dndcmp.sh Exec=dndcmp.sh %F Terminal=false Icon=edit-copy MimeType=application/octet-stream; Categories=Utility;FileTools;
#!/bin/sh # # dndcmp.sh # Drag'n'drop comparator # # Compares two files which are drag'n'dropped onto the companion file # dndcmp.desktop. That way all user I/O happens in a GUI fashion by # means of zenity dialogs. Nevertheless this shell script could be # executed from the command line as well. # # Check number of input arguments if [ $# -eq 1 ] then # Only one input argument. Open file selection dialog to let the user # pick the second file file1="$1" file2=$(zenity --file-selection --title="Please pick the second file") if [ $? -ne 0 ] then # Abort script if file selection dialog was aborted zenity --error --text="Two files required for comparison." exit 1 fi elif [ $# -eq 2 ] then # Two input arguments file1="$1" file2="$2" else # Neither one nor two input arguments. Abort script zenity --error --text="Please drag'n'drop one or two files." exit 1 fi # Make sure both inputs are regular files if [ ! -f "$file1" ] then zenity --error --text="$file1 is not a file." exit 2 fi if [ ! -f "$file2" ] then zenity --error --text="$file2 is not a file." exit 2 fi # Now do the actual comparison and inform the user about its result cmp --quiet "$file1" "$file2" # exit status: # 0 - same # 1 - different # 2 - trouble case $? in 0) zenity --title="Comparison result" --info \ --text="Both files contain the same data:\n$file1\n$file2" ;; 1) zenity --title="Comparison result" --warning \ --text="The two files differ:\n$file1\n$file2" ;; *) zenity --title="Comparison result" --error \ --text="An error occurred while comparing the files:\n$file1\n$file2" ;; esac
In order to run the tool, you need to mark both files as executable and have dndcmp.sh in your path. In order to do that, I executed the following commands on the command line:
chmod a+x dndcmp.sh dndcmp.desktop sudo mv dndcmp.sh /usr/local/bin/
Epilogue: Nobody’s perfect
In order to use the Drag’n’drop comparator, dndcmp.desktop has to be somewhere you can drop files onto. This could be the desktop, but I’d prefer to keep things un-cluttered and have the icon appear in the Unity launcher on my Ubuntu system. Unfortunately I was not able to drop files onto the icon once it was in the launcher. Even though I did quite some testing, I couldn’t figure out why the .desktop file for, say, gedit permits files to be dropped onto it while dndcmp.desktop didn’t. If you read this and know of a solution, do let me know, I would greatly appreciate it. Edited on January 8, 2013: For launcher items to accept any kind of file, the MIME type has to be application/octet-stream
whereas the previously used all/allfiles
doesn’t work. At least that’s my experience on Ubuntu 12.04 “Precise”. The file dndcmp.desktop above has been changed accordingly.
Thanks for this. I have modified it for meld. Do review it if you find the time.
#!/bin/bash
#
# dndcmp.sh
# Drag’n’drop comparator
#
# PROP=VALUE format buffer
HOME_DIR=~
BUFFER_STORE=”$HOME_DIR/.dndbuff”
BUFFER_STORE_TMP=”$HOME_DIR/.dndbufftmp”
BUFFER_KEY=”CMP_ITEM_BUFFER”
BUFFER_KEY_DELIM=”=”
#———————
# arg1: prop file
# arg2: prop key
# returns property value if found,
# nothing if not found
#———————
function fetch_buffer(){
#grep “^CMP_ITEM_BUFFER=” “/home/nishad/.dndbuff” | sed “s%”^CMP_ITEM_BUFFER=”=\(.*\)%\1%”
grep “^$BUFFER_KEY=” $BUFFER_STORE | sed “s%$BUFFER_KEY=\(.*\)%\1%”
}
function update_buffer(){
mv “$BUFFER_STORE” “$BUFFER_STORE_TMP”
# reset file with one key only
echo “$BUFFER_KEY$BUFFER_KEY_DELIM$BUFFER_VALUE” > “$BUFFER_STORE”
}
# This function is deprecated
# unless clear buffer preserve is implemented
function update_buffer_preserve(){
mv “$BUFFER_STORE” “$BUFFER_STORE_TMP”
#cat “/home/nishad/.dndbufftmp” | grep -v “^CMP_ITEM_BUFFER=” > “/home/nishad/.dndbuff” ;
# Use for preserving key and echo in append mode
cat “$BUFFER_STORE_TMP” | \
grep -v “^$BUFFER_KEY$BUFFER_KEY_DELIM” > “$BUFFER_STORE”
# reset file with one key only
echo “$BUFFER_KEY$BUFFER_KEY_DELIM$BUFFER_VALUE” >> “$BUFFER_STORE”
}
function clear_buffer(){
echo “$BUFFER_KEY=” > $BUFFER_STORE
}
# Cache buffer value
CMP_ITEM_BUFFER=$(fetch_buffer)
#zenity –title=”info” –info \
# –text=”Debug> args#=$# | arg1=$1 | arg2=$2 | buffer=$CMP_ITEM_BUFFER ”
# Check number of input arguments and buffer
if [ “$#” -eq 1 ] && [ -z “$CMP_ITEM_BUFFER” ]
then
# Only one input argument and empty buffer.
# Assign Inputs
item1=”$1″
BUFFER_VALUE=”$item1″
# zenity –title=”info” –info \
# –text=”Only one input argument, buffer empty> $1 $BUFFER_VALUE $CMP_ITEM_BUFFER ”
# Update buffer for next item
update_buffer
exit 0
elif [ “$#” -eq 1 ] && [ ! -z “$CMP_ITEM_BUFFER” ]
then
# Only one input argument and not empty buffer.
#zenity –title=”info” –info \
# –text=”Only one input argument, buffer NOT empty> $1.1 $BUFFER_VALUE $CMP_ITEM_BUFFER ”
# Assign Inputs
item1=”$CMP_ITEM_BUFFER”
item2=”$1″
# Clear Buffer
clear_buffer
elif [ “$#” -eq 2 ]
then
# Two input arguments
item1=”$1″
item2=”$2″
clear_buffer
else
# Neither one nor two input arguments. Abort script
zenity –error –text=”Please drag’n’drop one or two items.”
clear_buffer
exit 1
fi
if [ -f “$item1” ] && [ ! -f “$item2″ ]
then
zenity –title=”Error” –error –text=”Not Feasible”
clear_buffer; exit 1
elif [ -d “$item1” ] && [ ! -d “$item2″ ]
then
zenity –title=”Error” –error –text=”Not Feasible”
clear_buffer; exit 1exit 1
fi
# Now do the actual comparison and inform the user about its result
meld “$item1” “$item2”