dm-dm.sh - a dmenu oriented wget helper
dm-dm.sh <OPTIONS>
A shell script that handles wget downloads. It supports downloading, collecting statistics (downloaded size, percentage, speed, ETA) from wget's output, pausing, resuming and canceling. Since it's a generic shell script it could be used stand-alone or it could be integrated in any application (e.g. web browser) supporting the execution of external processes. Mandatory arguments to long options are mandatory to short options too.
-s, --start <URL>
begin a download of the file pointed by URL (output selection
is done with dmenu).
-p, --pause <WGET_PID>
pause a download held by wget's pid WGET_PID.
-r, --resume <WGET_PID>
resume a download held by wget's pid WGET_PID.
-c, --cancel <WGET_PID>
cancel a download held by wget's pid WGET_PID (it kills the
process but not delete the file).
-l, --list
list current downloads started by the script wih related
statistics.
-g, --gui
a GUI with dmenu to handle downloads' pause, resume and cancel.
-h, --help
print these informations.
-v, --version
print program version.
GNU coreutils, GNU sed, Bourne compatible shell, wget, dmenu and dmu-utils.sh script (it has to be set as executable and to be copied in
a path included in the $PATH environmental variable, because it's checked with which program).
dm-dm.sh uses dmenu for:
<Enter> serves as directory selection, <Shift><Enter> to confirm the selected directory and proceed with the script, <Escape> causes script to exit. The selection begins in the directory set in $DEFAULT_DOWNLOAD_DIRECTORY. With dmenu you could insert a custom path in the prompt (~ isn't supported). If a directory doesn't exists it'll prompt for directory creation.<Escape> is used to return to directory selection. It checks if a selected file exists, if yes it'll prompt for overwrite or for continue downloading on It. It also checks if other dm-dm.sh's download is on It.dm-dm.sh -g displays the output of dm-dm.sh -l through dmenu; <Enter> prompts download actions, <Shift><Enter> refreshes the list, <Escape> exits.<Enter> confirm action, <Escape> returns to downloads list.
Since dm-dm.sh's a generic script I suggest to copy It (and dm-utils.sh) outside uzbl's personal directory. I've got them in:
${HOME}/bin/dm-utils.sh${HOME}/bin/dm-dm.sh
${HOME}/bin is in ${PATH} (this is mandatory).
In uzbl's configuration file i set It up like this:
set home_bin = /home/bin set scripts_dir = @home_bin[:remaining paths it's up to you ;-)] set dm_dm_start = dm-dm.sh -s set download_handler = spawn @dm_dm_start @cbind !downloads = sh 'dm-dm.sh g'
stdout, i decided to avoid any error string, so if script fails It returns to shell with a plain exit 1Let me know if they comes to life, i'll try to fix them in the spare time.
#uzbl for testing#uzbl for testing
Finally here they are
#!/bin/sh
#
# dm-utils.sh - a set of dmenu oriented utilities
#
# Copyright (C) 2011 by Alessandro Massignan <ff0000.it@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
_WHICH="`which which 2>/dev/null`" || { exit 1; } # Required! ;-)
#
# Configuration variables
#
DMU_DEFAULT_DMENU_OPTS="-nb #303030 -nf #ffffff -sb #8b0000 -sf #ffffff"
DMU_DEFALT_DIRECTORY="${HOME}"
#
# System tools
#
_DMENU="`${_WHICH} dmenu 2>/dev/null`" || { exit 1; }
_FIND="`${_WHICH} find 2>/dev/null`" || { exit 1; }
_SED="`${_WHICH} sed 2>/dev/null`" || { exit 1; }
_PWD="`${_WHICH} pwd 2>/dev/null`" || { exit 1; }
_SORT="`${_WHICH} sort 2>/dev/null`" || { exit 1; }
_MKDIR="`${_WHICH} mkdir 2>/dev/null`" || { exit 1; }
_CUT="`${_WHICH} cut 2>/dev/null`" || { exit 1; }
#
# Functions
#
# description: a simple Q&A utility
# invocation: dmu_choice <$question> <$answer1, ..., $answerN>
# return: the choosen answer or empty string otherwise
dmu_choice()
{
Q="${1}" # question
A="" # answers
test "x${Q}" = "x" && { return 0; }
while test ! "x${1}" = "x"
do
shift
test "x${A}" = "x" && { A="${1}"; continue; }
A="${A}\n${1}"
done
test "x${A}" = "x" && { return 0; }
printf "`printf ${A} | \
${_DMENU} -p "${Q}" ${DMU_DEFAULT_DMENU_OPTS}`"
return 1
}
# description: a directory browser and selector
# invocation: dmu_directory_browse [$directory_tip]
# return: the chosen directory name or empty string otherwise
dmu_directory_select()
{
TD="" # target directory
SD="" # selected directory
DT="${1}" # directory tip
CD="`${_PWD}`" # current directory
(test ! "x`echo ${DT} | ${_CUT} -c1`" = "x/" && \
test ! "x${DT}" = "x") && { DT="${CD}/${DT}"; }
# Keys:
# * <Enter> - change to highlighted or prompt-inserted directory
# * <Shift> + <Enter> - select current directory
# * <Escape> - exit
while :
do
# dmenu's prompt
if test "x${TD}" = "x"; then
P="Select directory" # dmenu's prompt
else
P="Select directory or confirm '${TD}'"
fi
PD="" # prepend directories (tip, '.' and '..')
# If we are on '/', we don't need '..' anymore
if test ! "x${TD}" = "x/"; then
test ! "x${DT}" = "x" && { PD="${DT}\n"; }
PD="${PD}.\n..\n"
fi
# Small note: you couldn't choose '~' because it gets
# quoted so it hasn't recognized as $HOME directory
SD="`(printf "${PD}" && \
(${_FIND} . -maxdepth 1 -type d | \
${_SORT} | \
${_SED} -e 's/^\.\///g; /^\.$/d;')) | \
${_DMENU} -p \"${P}: \" ${DMU_DEFAULT_DMENU_OPTS}`"
# Escape key, plain exit
test ! $? -eq 0 && { cd $CD; return 0; }
# <Shift> + <Enter> - current directory selected, exit from loop
if test ! "x${TD}" = "x"; then
test "x${SD}" = "x" && { break; }
else
test "x${SD}" = "x" && { continue; }
fi
# A bit off-topic, but it's welcome sometimes :-)
if test ! "x${SD}" = "x" && test ! -d "${SD}"; then
Q="Directory \`${SD}' does not exist, create it?"
A="`dmu_choice "${Q}" Yes No`"
case $A in
Yes)
$_MKDIR -p $SD 2>/dev/null || \
{ cd $CD; return 1; }
;;
No)
SD="${TD}"
;;
*)
cd $CD
return 1
;;
esac
fi
cd $SD 2>/dev/null || { cd $CD; return 1; }
TD="`${_PWD}`"
done
printf "${TD}"
cd "${CD}"
return 0
}
# description: a file browser and selector
# invocation: dmu_file_browse <$directory> [$filename_tip]
# return: the chosen filename or empty string if no choice
dmu_file_select()
{
D="${1}" # start directory
TF="" # target filename
SF="" # selected file
FT="${2}" # filename tip
CD="`${_PWD}`" # current directory
test "x${D}" = "x" && { D="${DMU_DEFAULT_DIRECTORY}"; }
cd ${D} 2>/dev/null || { return 1; }
# Keys:
# * <Enter> - change to highlighted or prompt-inserted file
# * <Shift> + <Enter> - select current file
# * <Escape> - exit
while :
do
# dmenu's prompt
if test "x${TF}" = "x"; then
P="Select filename"
else
P="Select filename or confirm '${TF}'"
fi
PF="" # prepend filename tip
test ! "x${FT}" = "x" && { PF="${FT}\n"; }
test "x${TF}" = "x" && { TF="${FT}"; }
SF="`(printf "${PF}" && \
(${_FIND} . -maxdepth 1 -type f | \
${_SORT} | \
${_SED} -e 's/^\.\///g;')) | \
${_DMENU} -p \"${P}: \" ${DMU_DEFAULT_DMENU_OPTS}`"
# Escape key, plain exit
test ! $? -eq 0 && { return 1; }
# <Shift> + <Enter> - current file selected, exit from loop
(test ! "x${TF}" = "x" && test "x${SF}" = "x") && \
{ break; }
TF="${SF}"
done
printf "${TF}"
cd "${CD}"
return 0
}
#!/bin/sh
#
# dm-dm.sh - a dmenu oriented wget helper
#
# Copyright (C) 2011 by Alessandro Massignan <ff0000.it@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Credits:
# * inspired by lich-download.sh [http://www.uzbl.org/wiki/lich-download.sh]
#
# Thanks:
# * keis, bct from #uzbl - testing
#
_WHICH="`which which 2>/dev/null`" || { exit 1; } # Required! ;-)
#
# Configuration variables
#
WGET_OPTS="--user-agent Firefox"
TMP_DIRECTORY="/tmp"
DEFAULT_DOWNLOAD_DIRECTORY="${HOME}"
#
# Sourcing utilities
#
_DMUTILSSH="`${_WHICH} dm-utils.sh 2>/dev/null`" || { exit 1; }
. $_DMUTILSSH || { exit 1; }
#
# System tools
#
_WGET="`${_WHICH} wget 2>/dev/null`" || { exit 1; }
_CAT="`${_WHICH} cat 2>/dev/null`" || { exit 1; }
_BASENAME="`${_WHICH} basename 2>/dev/null`" || { exit 1; }
_DIRNAME="`${_WHICH} dirname 2>/dev/null`" || { exit 1; }
_MKTEMP="`${_WHICH} mktemp 2>/dev/null`" || { exit 1; }
_MKFIFO="`${_WHICH} mkfifo 2>/dev/null`" || { exit 1; }
_RM="`${_WHICH} rm 2>/dev/null`" || { exit 1; }
_CUT="`${_WHICH} cut 2>/dev/null`" || { exit 1; }
_SLEEP="`${_WHICH} sleep 2>/dev/null`" || { exit 1; }
_KILL="`${_WHICH} kill 2>/dev/null`" || { exit 1; }
_PS="`${_WHICH} ps 2>/dev/null`" || { exit 1; }
#
# Functions
#
usage()
{
S=""
if test ! "x${1}" = "x"; then
S=": ${1}"
fi
PN="`${_BASENAME} ${0}`"
$_CAT <<EOF
$PN $S
Try \`$PN --help\` for further informations.
EOF
}
help()
{
PN="`${_BASENAME} ${0}`"
$_CAT <<EOF
Usage: $PN <OPTIONS>
Handles wget downloads
Options:
-s,--start <URL> start downloading the passed URL
-p,--pause <WGET_PID> pause (or resume) a download
-c,--cancel <WGET_PID> cancel a download
-l,--list list downloads
-h,--help display these information
EOF
}
# Sanitize URL encoded characters and substitute multiple white spaces to '_'
sanitize_filename()
{
printf "${1}" | \
${_SED} -r \
-e 's/[_%]20/\_/g; s/[_%]22/\"/g; s/[_%]23/\#/g;' \
-e 's/[_%]24/\$/g; s/[_%]25/\%/g; s/[_%]26/\&/g;' \
-e 's/[_%]28/\(/g; s/[_%]29/\)/g; s/[_%]2C/\,/g;' \
-e 's/[_%]2D/\-/g; s/[_%]2E/\./g; s/[_%]2F/\//g;' \
-e 's/[_%]3C/\</g; s/[_%]3D/\=/g; s/[_%]3E/\>/g;' \
-e 's/[_%]3F/\?/g; s/[_%]40/\@/g; s/[_%]5B/\[/g;' \
-e 's/[_%]5C/\\\\/g; s/[_%]5D/\]/g; s/[_%]5E/\^/g;' \
-e 's/[_%]5F/\_/g; s/[_%]60/\`/g; s/[_%]7B/\{/g;' \
-e 's/[_%]7C/\|/g; s/[_%]7D/\}/g; s/[_%]7E/\~/g;' \
-e 's/[_%]2B/\+/g; s/\ \ */\_/g;'
return 0
}
# Start a backgrounded wget instance and statistics retrivial while loop
dm_start()
{
URL="${1}"
FILENAME="`sanitize_filename "\`printf "${URL}" | \
${_SED} -e 's/^.*\///'\`"`"
# Select output directory and file name with
# dmu_{file,directory}_select() helpers from dm-utils.sh
# Cursed shall be who uses white spaces in directory and
# file names ;-D (i've not tested this condition yet :-P)
OUTPUT=""
OUTPUT_DIRECTORY=""
OUTPUT_FILENAME=""
while :
do
test "x${OUTPUT_DIRECTORY}" = "x" && \
{ OUTPUT_DIRECTORY="${DEFAULT_DOWNLOAD_DIRECTORY}"; }
OUTPUT_DIRECTORY="`dmu_directory_select "${OUTPUT_DIRECTORY}"`"
test "x${OUTPUT_DIRECTORY}" = "x" && { return 0; }
test "x${OUTPUT_FILENAME}" = "x" && \
{ OUTPUT_FILENAME="${FILENAME}"; }
OUTPUT_FILENAME="`dmu_file_select "${OUTPUT_DIRECTORY}" \
"${OUTPUT_FILENAME}"`"
test "x${OUTPUT_FILENAME}" = "x" && { continue; }
break
done
OUTPUT="${OUTPUT_DIRECTORY}/${OUTPUT_FILENAME}"
# Ouch! Output file already exists, handling it
if test -r $OUTPUT; then
# Checking if other download process is writing to the same file
if test ! "x`echo ${TMP_DIRECTORY}/dm-info-*`" = \
"x${TMP_DIRECTORY}/dm-info-*"; then
for i in `echo $TMP_DIRECTORY/dm-info-*`
do
eval "`${_CAT} ${i}`"
test "x${OUTPUT}" = \
"x${DM_OUTPUT_DIRECTORY}/${DM_OUTPUT_FILENAME}" && \
{ return 1; }
done
fi
Q="File \`${OUTPUT}' already exists. Would you like to "
A="`dmu_choice "${Q}" Overwrite Continue Cancel`"
case $A in
Overwrite)
$_RM -f $OUTPUT 2>/dev/null || { return 1; }
;;
Continue)
WGET_OPTS="${WGET_OPTS} --continue"
;;
*)
return 1
;;
esac
fi
# Named pipe for wget statistics
STATS_PIPE="`${_MKTEMP} -u --tmpdir="${TMP_DIRECTORY}" dm-stats-XXXXXX`"
# Hoping Murphy doesn't create the same file in the between :-P
$_MKFIFO -m 0600 $STATS_PIPE
# Information file keeps all data we need
INFO_FILE="`${_MKTEMP} --tmpdir="${TMP_DIRECTORY}" dm-info-XXXXXX`"
WGET_PID=""
# Finally we could start downloading
$_WGET $WGET_OPTS -O "${OUTPUT}" "${URL}" >$STATS_PIPE 2>&1 &
WGET_PID=$!
# Basis information file
$_CAT >$INFO_FILE <<EOF
DM_WGET_PID="$WGET_PID"
DM_URL="$URL"
DM_OUTPUT_DIRECTORY=`$_DIRNAME $OUTPUT`
DM_OUTPUT_FILENAME=`$_BASENAME $OUTPUT`
DM_DOWNLOADED_SIZE=""
DM_PERCENTAGE=""
DM_SPEED=""
DM_ETA=""
EOF
# This is the loop in which we gather wget statistics from
# the named pipe and update the information file
{
${_CAT} $STATS_PIPE | \
while read L
do
# Ok, regexp from hell time! :-)
L="`echo "${L}" | \
${_SED} -nu 's/^[\ \t]*\([0-9][^\ \t]*\)[^0-9]*\([0-9][0-9]*%\)[\ \t][\ \t]*\([0-9][^\ \t]*\)[\ \t][\ \t]*\([0-9][^\ \t]*\).*$/DS\=\"\1\";\ P\=\"\2\";\ S\=\"\3\";\ E\=\"\4\";/p;'`"
eval "${L}"
eval "`${_CAT} ${INFO_FILE}`"
# Uploading (basically a rewritten, no time for file parsing :-P)
# information file everytime the percentage change (so 100
# writes). I choose this way because i don't need to be updated
# on every wget's output line... beeing on percentage is fine
# for me.
# Feel free to change this if you want :-)
if test ! "x${DM_PERCENTAGE}" = "x${P}"; then
$_CAT >$INFO_FILE <<EOF
DM_WGET_PID="$DM_WGET_PID"
DM_URL="$DM_URL"
DM_OUTPUT_DIRECTORY="$DM_OUTPUT_DIRECTORY"
DM_OUTPUT_FILENAME="$DM_OUTPUT_FILENAME"
DM_DOWNLOADED_SIZE="$DS"
DM_PERCENTAGE="$P"
DM_SPEED="$S"
DM_ETA="$E"
EOF
fi
done
# When wget ends the download or it's killed by someone else we've
# to remove the resources we used... I can't live with forgotten
# garbage.
$_RM -f $INFO_FILE $STATS_PIPE 2>/dev/null || { return 0; }
} &
return 0
}
# Produce a list of wget instances spawned by $0
dm_list()
{
# Sometimes 'echo' is a fine replacement to 'ls'
test "x`echo ${TMP_DIRECTORY}/dm-info-*`" = \
"x${TMP_DIRECTORY}/dm-info-*" && \
{ return 0; }
for i in `echo $TMP_DIRECTORY/dm-info-*`
do
eval "`${_CAT} ${i}`"
STATUS="`test x"\`${_PS} h -o state -p ${DM_WGET_PID}\`" = "xT" && \
{ printf "paused"; }`"
test "x${STATUS}" = "xpaused" && { DM_SPEED="-"; DM_ETA="-"; }
printf "%s %s/%s [%s, %s, %s, %s] %s\n" \
$DM_WGET_PID \
$DM_OUTPUT_DIRECTORY \
$DM_OUTPUT_FILENAME \
$DM_DOWNLOADED_SIZE \
$DM_PERCENTAGE \
$DM_SPEED \
$DM_ETA \
$STATUS
done
return 0
}
# Pause a download
dm_pause()
{
WGET_PID="${1}"
$_KILL -s STOP $WGET_PID || { return 1; }
return 0
}
# Resume a download, if we resume an active download (so a process)
# the CONT signal is harmless and it'll be ignored [resource kill(1)]
dm_resume()
{
WGET_PID="${1}"
$_KILL -s CONT $WGET_PID || { return 1; }
return 0
}
# Cancel a download, we don't perform file's removing because
# we're shell guys and we know 'rm' exists :-D
dm_cancel()
{
WGET_PID="${1}"
$_KILL -9 $WGET_PID || { return 1; }
return 0
}
# A simple dmenu driven GUI to handle dm_list, dm_pause, dm_resume and
# dm_cancel
dm_gui()
{
while :
do
# Calculating number of lines for dmenu's vertical displaying
LINES="`dm_list | ${_SED} -n '$='`"
# No lines, no downloads so a GUI is useless
test "x${LINES}" = "x" && { return 0; }
# Pick a download
PROMPT="Select a download:"
SELECTED_DOWNLOAD="`dm_list | \
${_DMENU} ${DMU_DEFAULT_DMENU_OPTS} \
-l ${LINES} -p "${PROMPT}"`"
# Keys:
# * <Enter> - select a download
# * <Shift> + <Enter> - refresh the list
# * <Escape> - exit
if test $? -eq 0; then
test "x${SELECTED_DOWNLOAD}" = "x" && { continue; }
else
return 0
fi
WGET_PID="`echo ${SELECTED_DOWNLOAD} | ${_CUT} -d' ' -f1`"
Q="Would you like to: "
A="`dmu_choice "${Q}" Pause Resume Cancel`"
if test ! $? -eq 0; then
test "x${A}" = "x" && { continue; }
else
return 0
fi
case $A in
Pause)
dm_pause $WGET_PID || { return 1; }
;;
Resume)
dm_resume $WGET_PID || { return 1; }
;;
Cancel)
dm_cancel $WGET_PID || { return 1; }
$_SLEEP 1s
;;
*)
# NOP
;;
esac
done
return 0
}
#
# Main
#
test "x${1}" = "x" && { usage "no option specified"; exit 1; }
# Parsing command line arguments
while test ! "x${1}" = "x"
do
case "${1}" in
-s|--start)
shift
dm_start "${1}" || { exit 1; }
break
;;
-p|--pause)
shift
dm_pause "${1}" || { exit 1; }
break
;;
-r|--resume)
shift
dm_resume "${1}" || { exit 1; }
break
;;
-c|--cancel)
shift
dm_cancel "${1}" || { exit 1; }
break
;;
-l|--list)
dm_list || { exit 1; }
break
;;
-g|--gui)
dm_gui || { exit 1; }
break
;;
-h|--help)
help || { exit 1; }
break
;;
*)
usage "unrecognized option \`${1}'"
exit 1
;;
esac
shift
done
# All done, see you on the next download! 8^)
exit 0