Contenu | Rechercher | Menus

Annonce

Si vous avez des soucis pour rester connecté, déconnectez-vous puis reconnectez-vous depuis ce lien en cochant la case
Me connecter automatiquement lors de mes prochaines visites.

À propos de l'équipe du forum.

#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 : GirollServices libres : TdCT.org
Hide in your shell, scripts & astuces :  applications dans un tunnelsmart wgettrouver des pdfinstall. auto de paquetssauvegarde 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é smile
Voire le module XML::Simple en Perl qui gere ça tres bien et facilement.
Suivant les cas, j'utilise l'un ou l'autre.


On ne peut pas mettre d'array dans un string!
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 wink 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... )


On ne peut pas mettre d'array dans un string!
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