Bonsoir,
Dans le cadre
des challenges de scripting, j'ai écris un parser XML en bash. Je le mets dans cette section car c'est un sujet récurrent (xml et bash) et qu'il peut s'avérer être utile.
J'ai essayé de traiter tous les cas mais je suis loin de connaître sur le bout des doigts le standard W3C XML. Merci de me remonter les problèmes que vous rencontrerez et éventuellement les axes d'amélioration.
#!/bin/bash
xmlBoolInit=false
xmlFichier=""
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
# méthode : xmlRechercheProperties
# Date de création : 24/04/2010
# Objet : Alimentation des tableaux des tags (xmlTags), valeurs (xmlValues) et d'indicateurs de valeur (xmlIsValuess)
# (Utilisé par xmlInit)
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
# entrée : liste des valeurs inclus dans un tag.
# Exemple :
# Pour le tag <?xml-stylesheet title="XSL formatting" type="text/xsl" href="http://planet.ubuntu-fr.org/feed/rss2/xslt" ?>,
# ce sera : title="XSL formatting" type="text/xsl" href="http://planet.ubuntu-fr.org/feed/rss2/xslt"
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
xmlRechercheProperties()
{
# on parcourt tous les paramètres
for xmlPropriete
do
# si le paramètre est bien au format <nom>=<valeur> ou au format <nom>:<valeur>
if [[ "${xmlPropriete}" = *[=:]* ]]; then
((idx++))
xmlIsValues[idx]=true
[[ "${xmlPropriete}" = *=* ]] && {
xmlTags[idx]="${xmlPropriete%%=*}"
xmlValues[idx]="$(awk -F= '{print $2}' <<< "${xmlPropriete}")"
} || {
xmlTags[idx]="${xmlPropriete%:*}"
xmlValues[idx]="$(awk -F: '{print $NF}' <<< "${xmlPropriete}")"
}
else
# ce n'est pas le cas, on considère que la valeur est associé au tag de l'indice courant.
xmlValues[idx]="${xmlPropriete}"
xmlIsValues[idx]=true
fi
done
}
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
# méthode : xmlNomTag
# Date de création : 24/04/2010
# Objet : Récupération du nom d'un tag d'après le format complet (<[\?/]?*[\?/]?>)
# (Utilisé par xmlInit)
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
# entrée : le tag
# Exemple : <?xml-stylesheet title="XSL formatting" type="text/xsl" href="http://planet.ubuntu-fr.org/feed/rss2/xslt" ?>,
#
# sortie : le nom du tag
# Exemple : xml-stylesheet
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
xmlNomTag()
{
# suppression du < en début
unTag="${1#<}"
# suppression du > en fin
unTag="${unTag%>}"
# suppression des caractères ? ou / en début
unTag="${unTag#[\?/]}"
# suppression des caractères ? ou / en fin
unTag="${unTag%[\?/]}"
# suppression de tout ce qui se trouve après le 1er espace et envoie en retour
echo "${unTag%% *}"
}
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
# méthode : xmlInit
# Date de création : 24/04/2010
# Objet : Initialisation du parser
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
# entrée : le nom du fichier à parser
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
xmlInit()
{
# on indique que le parser n'est pas initialisé
xmlBoolInit=false
xmlFichier="$1"
# chaine de travail
xmlString=
# liste des tags du fichier ( si un élément est vide, c'est que le tag correspond à une balise de fin. Ex. </xml>
xmlTags=()
# liste des valeurs de tag / propriété
xmlValues=()
# liste d'indicateur de valorisation d'un tag/propriété (true/false)
xmlIsValues=()
# vérification d'usage
[ $# -ne 1 ] && {
cat >&2 <<EOF
Utilisation :
${FUNCNAME[0]} <fichier.xml>
EOF
return 1
}
# vérification du fichier à analyser
[ ! -f "${xmlFichier}" ] && {
echo >&2 "${FUNCNAME[0]} : ${xmlFichier} non valide."
return 2
}
# on transforme tout le fichier en une seul chaine de caractère sans saut de ligne
xmlString="$(tr -d '\n' < "${xmlFichier}")"
wString="${xmlString}"
idx=0
# on considère que le parser est initialisé
xmlBoolInit=true
# pas de vérification de la case pour les comparaison de chaine
shopt -s nocasematch
while read
do
case "${REPLY}" in
\<\?* | \<*/\> | \<*[=:]*\> )
# on traite le cas des tags au format <?* (ex <?xml), <*/> (ex <chaine nom="sans nom"/>). Le cas \<*[=:]*\> est superfux mais ne mange pas de pain.
xmlTag="$(xmlNomTag "${REPLY}")"
# récupération de la liste des propriétés
xmlProperties="${REPLY#*${xmlTag}}"
xmlProperties="${xmlProperties%>}"
xmlProperties="${xmlProperties%[\?/]}"
xmlTags[idx]="${xmlTag}"
xmlIsValues[idx]=false
# gestion des propriétés
eval "xmlRechercheProperties ${xmlProperties}"
# doit-on fermer le tag ? Oui si il est de la forme [?/]>
[[ "${REPLY}" = *[\?/]\> ]] && {
((idx++))
xmlTags[idx]=""
}
;;
\<[^\?/]*[^?/]\>*)
# on traite le cas d'un tag simple : <nom>
xmlTags[idx]="$(xmlNomTag "${REPLY}")"
xmlIsValues[idx]=false
;;
\</*)
# on traite le cas des tags de fin (</nom>)
xmlTag="$(xmlNomTag "${REPLY}")"
xmlIsValues[idx]=false
# est-ce un tag de fermeture du tag précédant ?
[ "${xmlTags[$((idx-1))]}" = "${xmlTag}" ] && {
# oui, alors la valeur du tag précédant est identifiée par tout ce qui précède REPLY dans wString
((idx--))
xmlValues[idx]="$(awk -F"${REPLY}" '{print $1}' <<< "${wString}")"
xmlIsValues[idx]=true
} || xmlTags[idx]="" # non, alors tag de fin "normal"
;;
esac
# on supprime de wString tout ce qui précède REPLY et REPLY
wString="${wString#*${REPLY}}"
((idx++))
done < <( egrep -o '<[/\?]?[^\?/>]*([^\?/>]+=("[^"]*"|[^ >]*))*[^\?/>][/\?]?>' <<< "${xmlString}") # listing de tous les tags de la chaine xmlString
shopt -u nocasematch
xmlDebutRecherche
}
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
# méthode : xmlAffiche
# Date de création : 24/04/2010
# Objet : Affichage d'un arbre des tags
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
xmlAffiche()
{
${xmlBoolInit} || {
echo "${FUNCNAME[0]} : Le parser n'a pas été initialisé."
return 1
}
echo "${xmlFichier}"
decalage=""
indent="|"$'\t'
[ ${#xmlTags[@]} -eq 0 ] && echo "<Rien à afficher>"
for((idx=0;idx < ${#xmlTags[@]}; idx++))
do
[ "${xmlTags[idx]}" ] && {
${xmlIsValues[idx]} && echo -e "${decalage}${xmlTags[idx]}=${xmlValues[idx]}" || {
echo "${decalage}${xmlTags[idx]}:"
decalage="${decalage}${indent}"
}
} || decalage="${decalage%${indent}}"
done
}
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
# méthode : xmlDebutRecherche
# Date de création : 24/04/2010
# Objet : Initialisation des variables de recherche
# A appeler avant utilisation de xmlRecherche
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
xmlDebutRecherche()
{
xmlIdxSearch=-1
unset xmlPath[@]
}
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
# méthode : xmlRecherche
# Date de création : 24/04/2010
# Objet : Recherche la valeur d'un tag
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
# entrée : le nom du tag à rechercher
# Ce nom peut être un simple nom ou une arborescence complète. Si c'est le cas, il faut le préciser avec l'option -F
# (à spécifier avant le nom du tag)
#
# sortie : les variables xmlValeurTrouvee ( valeur du tag) et xmlCheminTrouve (chemin pour y accèder)
#-------------------------------------------------------------------------------------------------------------------------------------------------------------------
xmlRecherche()
{
${xmlBoolInit} || {
echo "${FUNCNAME[0]} : Le parser n'a pas été initialisé."
return 1
}
xmlFullSearch=false
xmlLastIdx=${xmlIdxSearch}
xmlValeurTrouvee=""
xmlCheminTrouve=""
case $# in
1) xmlSearch="$1"
;;
2) xmlSearch="$2"
[ "$1" = "-F" ] || {
echo "${FUNCNAME[0]} : Option '$1' non valide.'"
return 1
}
xmlFullSearch=true
xmlDebutRecherche
;;
*) cat <<EOF
${FUNCNAME[0]} : Nombre de parametre non valide ! 1 ou 2 attendu.
[-F full-path]|[tag]
EOF
;;
esac
[ ! "${xmlSearch}" ] && {
echo "${FUNCNAME[0]} : Pattern à rechercher non valide..'"
return 1
}
xmlPatternFound=false
xmlEndReached=false
shopt -s nocasematch
until ${xmlPatternFound} || ${xmlEndReached}
do
((xmlIdxSearch++))
(( xmlIdxSearch >= ${#xmlTags[@]} )) && {
# on a atteint la fin, on stope la recherche et on repositionne l'index
xmlEndReached=true
xmlIdxSearch=${xmlLastIdx}
} || {
if [ "${xmlTags[xmlIdxSearch]}" ] ; then
# il ne s'agit pas élément de tag de fin
xmlPath+=( ${xmlTags[xmlIdxSearch]} )
allPath=${xmlPath[@]}
if ${xmlFullSearch}; then
# cas où c'est un chemin complet que l'on recherche
[ "${allPath}" = "${xmlSearch}" ] && {
xmlPatternFound=true
xmlValeurTrouvee=${xmlValues[xmlIdxSearch]}
xmlCheminTrouve=${allPath}
}
else
# cas d'un simple tag
[ "${xmlTags[xmlIdxSearch]}" = "${xmlSearch}" ] && {
xmlPatternFound=true
xmlValeurTrouvee=${xmlValues[xmlIdxSearch]}
xmlCheminTrouve=${allPath}
}
fi
# s'il ne s'agit pas d'un tag valorisé, on remonte d'un palier
${xmlIsValues[xmlIdxSearch]} && unset xmlPath[$((${#xmlPath[@]}-1))]
else
# élément de tag de fin, on remonte d'un palier
unset xmlPath[$((${#xmlPath[@]}-1))]
fi
}
done
shopt -u nocasematch
# si on a rien trouvé, on affiche un msg
${xmlEndReached} && { echo "'${xmlSearch}' not found !" >&2 ; return 1; }
return 0
}
EDIT : Pour l'utiliser, il faut sourcer le script et appeler dans un premier temps la méthode xmlInit.
Ensuite, vous avez les méthodes xmlAffiche (affichage d'un arbre des tags) et xmlRecherche pour rechercher un tag.
Je pensais écrire une méthode pour parcourir les tags mais il s'agit d'une copie de la méthode xmlAffiche donc l'affichage est à remplacer par vos traitements particuliers.