BashWget est une imitation très limitée de l’utilitaire GNU Wget.
Il s’agit d’une imitation très limitée car le but n’était pas de réécrire entièrement Wget mais de montrer quelques-unes des fonctionnalités avancées de Bash.
Le script BashWget est entièrement écrit en Bash et n’utilise quasiment que des fonctionnalités internes de Bash (à l’exception de la commande cat) :
Un des avantages de n’utiliser que des fonctions internes de Bash est d’accélérer la vitesse du script car on limite fortement le lancement de sous-processus.
Le code source se trouve en bas de cette page.
La variable $CRLF
contient le code de deux octets nécessaires à la génération d’un retour à la ligne compatible avec les en-têtes HTTP. CR pour Carriage Return et LF pour Line Feed.
Pour créer cette variable on utilise les chaînes type C de Bash. En écrivant CRLF=$'\r\n'
, au lieu de CRLF="\r\n"
, Bash va remplacer \r
par le caractère de contrôle 0x0d
(Carriage Return) et \n
par le caractère de contrôle 0x0a
(Line Feed). Dans le deuxième cas, c’est la chaîne \r\n
(4 caractères) qui aurait été affectée à CRLF.
Bash permet de définir des variables locales dans les fonctions. C’est très pratique pour éviter les effets de bords car, par défaut, les variables sont globales.
Cela permet également d’indiquer comment sont utilisés les paramètres de la fonction (les fonctions sous Bash n’ont pas de paramètres nommés). Par exemple, pour la fonction http_request
, la définition des variables locales se fait en même temps que leur initialisation :
local method="$1" host="$2 path="$3"
On voit donc au premier coup d’œil que la fonction http_request
prend trois paramètres :
method
, host
et path
.
Bash est capable de se connecter en TCP à un serveur grâce à des chemins spéciaux. Ceux-ci n’ont aucune existence du point de vue système, seul Bash effectue le lien.
Pour se connecter à un serveur Web, le protocole TCP est utilisé avec un chemin spécial de la forme /dev/tcp/hôte/port
. Par exemple, pour se connecter à http://www.example.com, on utilisera le chemin /dev/tcp/www.example.com/80
.
Pour établir la connexion, on fait appel à la fonction exec
de Bash :
exec 3<> "/dev/tcp/www.example.com/80"
Cette commande demande à Bash de connecter le descripteur de fichiers 3 en lecture/écriture (<>
) sur la socket connectée à www.example.com sur le port 80.
La lecture du chapitre suivant est fortement recommandée…
La fonction exec
de Bash permet normalement de remplacer le processus courant par un autre (donc sans en lancer de nouveau).
Mais, comme souvent en Bash, elle a d’autres fonctionnalités qui n’ont pas grand chose à voir avec le but initial.
La fonction exec
permet de modifier les descripteurs de fichiers. Bien que nous disposions déjà des redirections et des tubes, il existe au moins un cas où ceux-ci ne peuvent être utilisés : lorsqu’on utilise la fonction exec
elle-même !
Dans le précédent chapitre était présenté l’appel exec 3<> "/dev/tcp/www.example.com/80"
. Que se passe-t-il si jamais la connexion au serveur ne peut être établie ? La fonction exec
envoie alors un message d’erreur sur la sortie standard. Si on veut contrôler l’affichage des messages d’erreur, il n’est malheureusement pas possible d’utiliser une redirection sur la fonction exec
. Si on tente un exec 2>/dev/null
, Bash comprendra que l’on veut rediriger la sortie d’erreur du processus en cours sur /dev/null
.
Pour contourner le problème, on va utiliser exec
pour effectuer une sauvegarde de la sortie d’erreur courante dans un descripteur de fichier inutilisé (4>&2
) puis la rediriger vers /dev/null
(2> /dev/null
) :
exec 4>&2 2> /dev/null
On utilise à nouveau exec
pour restaurer la sortie d’erreur (2>&4
) et fermer le descripteur de fichier utilisé pour la sauvegarde (4>&-
) :
exec 2>&4 4>&-
La fonction http_request
sert uniquement à générer une requête minimale mais propre qui répond aux standards.
Elle prend 3 paramètres :
La fonction http_request
utilise la fonction
printf
pour générer l’entête HTTP car elle permet un contrôle fin des séquences de caractères générées notamment pour le retour à la ligne qui diffère du standard Unix.
La fonction http_request
se présente ainsi :
function http_request {
local method="$1" host="$2" path="$3"
printf '%s %s HTTP/1.0\r\n' "$method" "$path"
printf 'User-Agent: BashWGet\r\n'
printf 'Accept: */*\r\n'
printf 'Host: %s\r\n' "$host"
printf 'Connection: close\r\n'
printf '\r\n'
}
Les différents printf
vont générer dans l’ordre pour l’appel http_request GET www.example.com /
:
cat
pour lire sur la socket et ne s’arrête que quand celle-ci a été fermée à l’autre bout)Bash fournit un ensemble de facilités pour manipuler les chaînes de caractères sans devoir recourir aux outils classiques tels que cut
, sed
, grep
etc.
Je parle de facilités car ce ne sont pas des fonctions ou des commandes. Ces manipulations se font via l’expansion des paramètres shell.
Pour émuler la fonction strlen
en Bash, il suffit d’utiliser un
#
comme suit :
${#variable}
L’expansion va retourner le nombre de caractères. Exemple :
variable='Frédéric'
${#variable} → 8
La fonction substr
des langages évolués peut être remplacée par l’expansion suivante :
${variable:debut:longueur}
Quelques exemples :
variable='Frédéric'
${variable:0:5} → Frédé
${variable: -4} → éric
:
et le -4
est nécessaire sinon Bash confondra avec un autre type d’expansionBash permet de supprimer des caractères d’une chaîne en se basant sur des motifs semblables à ceux utilisés pour désigner des fichiers sur la ligne de commande.
Les quatre formes sont les suivantes :
${variable#motif}
${variable##motif}
${variable%motif}
${variable%%motif}
#
et ##
permettent de supprimer un motif en début de chaîne tandis que %
et %%
permettent de supprimer un motif en fin de chaîne. La différence entre les simples et les doubles tient dans le fait que les simples suppriment le plus petit morceau de chaîne possible alors que les doubles suppriment le plus grand morceau de chaîne possible.
Quelques exemples :
variable='www.example.com/chemin/vers/ressource'
${variable#*/} → chemin/vers/ressource
${variable##*/} → ressource
${variable%/*} → www.example.com/chemin/vers
${variable%%/*} → www.example.com
On s’aperçoit immédiatement qu’il est ainsi très simple de couper une chaîne en deux par rapport à un séparateur :
variable='www.example.com/chemin/vers/ressource'
${variable%%/*} → www.example.com
${variable#*/} → chemin/vers/ressource
Par exemple, si on veut découper une URL en Bash :
url='http://www.example.com:8080/chemin/vers/ressource'
hostportpath="${url#http://}" → www.example.com:8080/chemin/vers/ressource
path="${hostportpath#*/}" → chemin/vers/ressource
hostport="${hostportpath%%/*}" → www.example.com:8080
host="${hostport%:*}" → www.example.com
port="${hostport#*:}" → 8080
Télécharger bashwget.xz Taille du fichier : 1,9 Ko
#!/usr/bin/env bash
# CRLF is the fields separator in the HTTP headers
CRLF=$'\r\n'
function http_request {
local method="$1" host="$2" path="$3"
printf '%s %s HTTP/1.0\r\n' "$method" "$path"
printf 'User-Agent: BashWGet\r\n'
printf 'Accept: */*\r\n'
printf 'Host: %s\r\n' "$host"
printf 'Connection: close\r\n'
printf '\r\n'
}
function http_get {
local host="$1" port="$2" path="$3"
# Save standard error on file descriptor 4 and redirects it to /dev/null
# We do this because we cannot directly redirect standard error of the next
# exec command
exec 4>&2 2> /dev/null
# Open a connection to the host with a specific port
exec 3<> "/dev/tcp/$host/$port"
rc=$?
# Restore standard error from file descriptor 4
exec 2>&4 4>&-
[ $rc -ne 0 ] && return $rc
# Send the request
http_request GET "$host" "$path" >&3
# Read the answer
cat <&3
}
function parse_url {
local url="$1" hostportpath path hostport host port
# Parse URL
hostportpath="${url#http://}" # Remove protocol, get host, port and path
path="${hostportpath#*/}" # Remove host from hostportpath to get path
hostport="${hostportpath%%/*}" # Remove path from hostportpath to get hostport
host="${hostport%:*}" # Remove port from hostport to get host
port="${hostport#*:}" # Remove host from hostport to get port
# Due to nature of Bash string manipulation, if $path is the same as
# $hostportpath if means the user hasn't specified a path
test "$path" == "$hostportpath" && path=""
# Due to nature of Bash string manipulation, if $host is the same as $hostport
# if means the user hasn't specified a port. Defaults to 80
test "$host" == "$hostport" && port=80
# Add the preceding slash to the path, because it was removed when parsed
path="/$path"
printf '%s %s %s' "$host" "$port" "$path"
}
function apply_location {
local host="$1" port="$2" path="$3" location="$4"
# Determine if the location is absolute or relative
if [ "${location:0:7}" == "http://" ]
then
# Location is absolute
set $(parse_url "$location")
host="$1" port="$2" path="$3"
else
# Location is relative, there are 3 cases to handle
if [ "${location:0:1}" == '/' ]
then
# Location is absolute relatively to the host
path="$location"
else
if [ "${path: -1}" == '/' ]
then
# The current path is a directory, location is relative to it
path="$path$location"
else
# The current path is a document, location is relative to its
# directory
path="$(dirname "$path")/$location"
fi
fi
fi
printf '%s %s %s' "$host" "$port" "$path"
}
function usage {
cat <<EOF
usage: $0 [-h] [-e] [-b] <url>
BashWget is a free utility for non-interactive download of files from the Web.
It supports HTTP only. It’s a very limited copy of the GNU Wget utility.
Options are cumulative.
OPTIONS:
-h show this message
-e output HTTP header response
-b output HTTP body response
-c output status code
-q quiet mode
url URL to retrieve, only HTTP is supported with optional port and path
url must be the last parameter
EOF
}
# Parse arguments
showheader="" showbody="" showstatus="" quietmode=""
while getopts "hebcq" OPTION
do
case $OPTION in
h) usage ; exit 1 ;;
e) showheader="on" ;;
b) showbody="on" ;;
c) showstatus="on" ;;
q) quietmode="on" ;;
?) usage ; exit ;;
esac
done
# Delete options we have already processed
shift $((OPTIND-1))
# Parse URL
set $(parse_url "$1")
host="$1" port="$2" path="$3"
while [ "$path" ]
do
[ "$quietmode" ] || printf 'Trying [%s] [%s] [%s]\n' "$host" "$port" "$path" >&2
# Run the HTTP request
answer=$(http_get "$host" "$port" "$path")
# Split answer into header and body
header="${answer%%$CRLF$CRLF*}"
body="${answer#*$CRLF$CRLF}"
# Due to nature of Bash string manipulation, if $header is the same as
# $body
test "$header" == "$answer" && body=""
# Analyze the status
status=( ${header%%$CRLF*} ) # Split the first line of the header
status_code="${status[1]}" # Status code is the second parameter
# Look for a Location line in the header
location="${header#*${CRLF}Location: }"
if [ "$location" != "$header" ]
then
# A Location line has been found, get the destination
location="${location%%$CRLF*}"
# Apply location to the current URL
set $(apply_location "$host" "$port" "$path" "$location")
host="$1" port="$2" path="$3"
else
# There is nothing left to do, end the while loop
location=""
path=""
fi
done
test "$showstatus" && printf '%s\n' "$status_code"
test "$showheader" && printf '%s' "$header"
test "$showbody" && printf '%s' "$body"