Pages : 1
#1 Le 26/04/2010, à 20:58
- Totor
[Bash][XML] Un parser
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.
Dernière modification par Totor (Le 26/04/2010, à 21:03)
-- Lucid Lynx --
Hors ligne
#2 Le 26/04/2010, à 22:36
- nesthib
Re : [Bash][XML] Un parser
Joli travail encore une fois Totor. J'ai ajouté ton fil à la liste des épinglés.
GUL Bordeaux : Giroll – Services libres : TdCT.org
Hide in your shell, scripts & astuces : applications dans un tunnel – smart wget – trouver des pdf – install. auto de paquets – sauvegarde auto – ♥ awk
⃛ɹǝsn xnuᴉꞁ uʍop-ǝpᴉsdn
Hors ligne
#3 Le 28/04/2010, à 02:09
- sputnick
Re : [Bash][XML] Un parser
Heu, avec Xpath et xmllint c'est quand même plus adapté
Voire le module XML::Simple en Perl qui gere ça tres bien et facilement.
Suivant les cas, j'utilise l'un ou l'autre.
Il existe un tunnel obscur dans la lumière infinie. Lao-Tseu
https://sputnick.fr
Hors ligne
#4 Le 28/04/2010, à 19:43
- Totor
Re : [Bash][XML] Un parser
Oui, effectivement.
Tu les avais déjà cité à plusieurs reprises. C'est pourquoi je l'avais mentionné ici. Je n'ai pas pris le temps de les rechercher car je l'avais aussi pris comme un challenge Je suis également conscient que ma solution a une faiblesse (les sauts de lignes sont perdus).
-- Lucid Lynx --
Hors ligne
#5 Le 28/04/2010, à 20:55
- sputnick
Re : [Bash][XML] Un parser
Le XML est un standard, il a été conçu comme une passerelle entre des langages hétérogènes et donc son implémentation se doit de rester standard. Là tu essaye de réinventer une roue qui tourne ma fois très rond. Je suis moi aussi un inconditionnel de bash, mais dans certains cas j'estime qu'il ne faut pas s'acharner. Sinon, quitte à relever des défis, il est plus utile d'apprendre les standards.
Quand on a gouté à
xmllint --shell fichier.xml
, on comprends la force de XML version XPath. ( on peux se déplacer dans l'arborescence avec cd, faire des ls, des cat... )
Il existe un tunnel obscur dans la lumière infinie. Lao-Tseu
https://sputnick.fr
Hors ligne
#6 Le 21/04/2011, à 09:46
- RoxPro
Re : [Bash][XML] Un parser
Justement je voudrais savoir si je veux analyser un fichier XML, je cree des fichiers de conf avec differents tags, par contre sur les sous-tag, je les mets comme le contenu de fichier de conf, t'as dis que ca va pas marcher les changements de lignes, est-ce que ca veut dire que je dois une autre solution pour analyser le fichier xml? merci
Hors ligne
Pages : 1