{"id":252,"date":"2026-10-13T03:11:09","date_gmt":"2026-10-13T03:11:09","guid":{"rendered":"http:\/\/www.scmgalaxy.com\/tutorials\/2010\/05\/03\/reading-xml-file-using-shell-script\/"},"modified":"2026-10-13T03:11:09","modified_gmt":"2026-10-13T03:11:09","slug":"reading-xml-file-using-shell-script","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/blog\/reading-xml-file-using-shell-script\/","title":{"rendered":"Reading XML in Shell Scripts: Safer Patterns with xmlstarlet and xmllint"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Reading XML directly in shell scripts is common in lightweight automation, legacy jobs, and restricted runtime environments. This guide explains the classic shell-only pattern, where it breaks, and how to extract XML values more reliably with XPath-capable command-line tools.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Originally published on 2010-05-03; fully revised and updated.<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><img decoding=\"async\" src=\"https:\/\/www.devopsschool.com\/blog\/wp-content\/uploads\/2010\/05\/xml-file-using-shell-script.png\" alt=\"Reading XML file using shell script\" \/><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The XML shape this script pattern targets<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The original post aimed to read specific fields (changelog, name, date, email) from XML using Bash only. A representative XML file for that use case looks like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">entry<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">changelog<\/span>&gt;<\/span>Fixed a new bug<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">changelog<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">name<\/span>&gt;<\/span>Shoaib Mir<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">name<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">date<\/span>&gt;<\/span>Sun, 02 May 2010<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">date<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">email<\/span>&gt;<\/span>shoaibmir&#91;@]gmail.com<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">email<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">entry<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">What the original grep\/sed approach does<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The original technique loops over tag names, uses <code>grep<\/code>\/<code>sed<\/code> to strip tags, then assigns values to variables. A simplified equivalent is:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php language-bash\"><span class=\"hljs-comment\">#!\/usr\/bin\/env bash<\/span>\n\n<span class=\"hljs-keyword\">for<\/span> key in changelog name date email; <span class=\"hljs-keyword\">do<\/span>\n  output=<span class=\"hljs-string\">\"$(grep \"<\/span>$key<span class=\"hljs-string\">\" log.xml | tr -d '\\t' | sed -e \"<\/span>s<span class=\"hljs-comment\">#&lt;${key}&gt;##\" -e \"s#&lt;\/${key}&gt;##\")\"<\/span>\n  <span class=\"hljs-keyword\">eval<\/span> <span class=\"hljs-string\">\"${key}=\\\"${output}\\\"\"<\/span>\ndone\n\n<span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">\"$changelog\"<\/span>\n<span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">\"$name\"<\/span>\n<span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">\"$date\"<\/span>\n<span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">\"$email\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This can work for tightly controlled, single-line XML, but it is <strong>fragile<\/strong> for nested elements, attributes, namespaces, multiline text, or repeated nodes. Using <code>eval<\/code> also increases risk if input is not trusted.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Use XPath tools for safer shell scripts<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Option 1: xmlstarlet (shell-friendly XPath queries)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If <code>xmlstarlet<\/code> is available, it is usually the cleanest CLI option for extracting XML fields in Bash.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php language-bash\"><span class=\"hljs-comment\">#!\/usr\/bin\/env bash<\/span>\nset -euo pipefail\n\nfile=<span class=\"hljs-string\">\"${1:-log.xml}\"<\/span>\nkeys=(changelog name date email)\n\n<span class=\"hljs-keyword\">if<\/span> ! command -v xmlstarlet &gt;\/dev\/<span class=\"hljs-keyword\">null<\/span> <span class=\"hljs-number\">2<\/span>&gt;&amp;<span class=\"hljs-number\">1<\/span>; then\n  <span class=\"hljs-keyword\">echo<\/span> <span class=\"hljs-string\">\"xmlstarlet is required but not installed\"<\/span> &gt;&amp;<span class=\"hljs-number\">2<\/span>\n  <span class=\"hljs-keyword\">exit<\/span> <span class=\"hljs-number\">1<\/span>\nfi\n\n<span class=\"hljs-keyword\">for<\/span> key in <span class=\"hljs-string\">\"${keys&#91;@]}\"<\/span>; <span class=\"hljs-keyword\">do<\/span>\n  value=<span class=\"hljs-string\">\"$(xmlstarlet sel -t -v \"<\/span>\/entry\/${key}<span class=\"hljs-string\">\" -n \"<\/span>$file<span class=\"hljs-string\">\")\"<\/span>\n  printf <span class=\"hljs-string\">'%s: %s\\n'<\/span> <span class=\"hljs-string\">\"$key\"<\/span> <span class=\"hljs-string\">\"$value\"<\/span>\ndone<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Option 2: xmllint with XPath<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>xmllint<\/code> can also extract scalar values with XPath and is commonly present where libxml2 tooling exists.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php language-bash\"><span class=\"hljs-comment\">#!\/usr\/bin\/env bash<\/span>\nset -euo pipefail\n\nfile=<span class=\"hljs-string\">\"${1:-log.xml}\"<\/span>\n\n<span class=\"hljs-keyword\">for<\/span> key in changelog name date email; <span class=\"hljs-keyword\">do<\/span>\n  value=<span class=\"hljs-string\">\"$(xmllint --xpath \"<\/span>string(\/entry\/${key})<span class=\"hljs-string\">\" \"<\/span>$file<span class=\"hljs-string\">\" 2&gt;\/dev\/null)\"<\/span>\n  printf <span class=\"hljs-string\">'%s: %s\\n'<\/span> <span class=\"hljs-string\">\"$key\"<\/span> <span class=\"hljs-string\">\"$value\"<\/span>\ndone<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">Approach comparison<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Method<\/th><th>Strengths<\/th><th>Limitations<\/th><th>Best fit<\/th><\/tr><\/thead><tbody><tr><td>grep + sed (+ eval)<\/td><td>No extra XML tool required; fast for trivial input<\/td><td>Brittle parsing; unsafe patterns with untrusted input; fails on many valid XML structures<\/td><td>Short-lived scripts with strictly controlled XML format<\/td><\/tr><tr><td>xmlstarlet sel<\/td><td>Real XML parsing with XPath; script-friendly output<\/td><td>Requires xmlstarlet availability<\/td><td>Reliable extraction in CI\/CD scripts and automation jobs<\/td><\/tr><tr><td>xmllint &#8211;xpath<\/td><td>Real XML parsing; XPath support; often available in Unix-like environments<\/td><td>XPath output\/error handling can need extra care<\/td><td>Environments where xmllint is already installed<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Decision flow for shell-based XML extraction<\/h2>\n\n\n\n<pre class=\"mermaid\">\nflowchart TD\n    A[Need XML values in a shell script] --> B{XML input is strictly controlled}\n    B -- yes --> C{Only simple flat tags}\n    C -- yes --> D[Temporary grep sed approach]\n    C -- no --> E[Use XPath parser]\n    B -- no --> E[Use XPath parser]\n    E --> F{xmlstarlet available}\n    F -- yes --> G[Use xmlstarlet sel]\n    F -- no --> H{xmllint available}\n    H -- yes --> I[Use xmllint xpath]\n    H -- no --> J[Add XML tool dependency]\n    D --> K[Document limits and migration plan]\n    G --> L[Add validation and error handling]\n    I --> L\n    J --> L\n<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Best practices<\/h2>\n\n\n\n<ul class=\"wp-block-list\"><li>Prefer real XML parsers (<code>xmlstarlet<\/code> or <code>xmllint<\/code>) over regex-style parsing.<\/li><li>Avoid <code>eval<\/code> unless you completely control input and variable names.<\/li><li>Use <code>set -euo pipefail<\/code> in automation scripts and fail fast on missing tools.<\/li><li>Validate input files before extraction and handle empty\/missing nodes explicitly.<\/li><li>If you must keep grep\/sed temporarily, document assumptions (single-line tags, no nesting, no namespaces).<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Reference and attribution<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This revision is based on the original DevOpsSchool post and its cited source: <a href=\"https:\/\/www.devopsschool.com\/blog\/reading-xml-file-using-shell-script\/\">How to read XML file by using shell script?<\/a> and <a href=\"https:\/\/shoaibmir.wordpress.com\/2010\/05\/02\/reading-xml-file-using-shell-scripting\/\">Reading XML file using shell scripting<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Need to extract values from an XML file in a shell script? This revised guide explains the classic grep\/sed approach, its limits, and safer XPath-based alternatives for production automation.<\/p>\n","protected":false},"author":1,"featured_media":4367,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_joinchat":[],"footnotes":""},"categories":[454],"tags":[349,1122,3113,1806,593,4115,4116,4120,4118,278,138,3046,4119,1927,524,4117],"class_list":["post-252","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-shell-script","tag-file","tag-guide","tag-how-to","tag-instruction","tag-process","tag-read","tag-read-xml-file","tag-read-xml-file-using-shell-script","tag-read-xml-with-shell-script","tag-script","tag-shell","tag-shell-script","tag-shell-script-for-reading-xml-content","tag-using","tag-xml","tag-xml-file-reading-by-shell-script"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/252","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/comments?post=252"}],"version-history":[{"count":3,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/252\/revisions"}],"predecessor-version":[{"id":79080,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/252\/revisions\/79080"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/media\/4367"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/media?parent=252"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/categories?post=252"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/tags?post=252"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}