xmlstarlet returns 1 with empty value - exit-code

I'm tring use xmlstarlet for parsing this file.xml:
<resultset>
<row>
<field name="b">2</field>
<field name="c"></field>
</row>
</resultset>
With first field evrithing is ok
$ xmlstarlet sel -t -v "resultset/row[1]/field[1]" file.xml
2
$ echo $?
0
But with second field xmlstarlet returns 1
$ xmlstarlet sel -t -v "resultset/row[1]/field[1]" file.xml
$ echo $?
1
In my case empty fields are normal. I want to parse its without xmlstarlet error.
UPDATE:
the same behavior with field[#name="b"]:
$ xmlstarlet sel -t -v 'resultset/row/field[#name="b"]' file.xml
2
$ echo $?
0
and
$ xmlstarlet sel -t -v 'resultset/row/field[#name="c"]' file.xml
$ echo $?
1
i want to distinguish the second case from the real error
UPDATE 2: The MAIN PROBLEM is:
If i try select [#name="c"] and [#name="not_exits"] xmlstarlet returns the SAME exit code 1.
But file.xml has field name 'c', and does not have field with name 'not_exist'.
I want xmlstarlet prints empti string and return exit code 0 when file.xml contains empty field with the given name,
and returns non-null exit code when file.xml does not contain field with the given name at all.

Try changing you xpath expressions to
resultset/row/field[#name="b"]
and
resultset/row/field[#name="c"]
and see if it works.
Edit:
I'm not sure what exactly you are echoing in your question, but if we assume your xml is in a file called file.xml, these expressions work with no errors:
xmlstarlet sel -T -t -m "resultset/row/field[#name='b']" -v . --nl file.xml
and
xmlstarlet sel -T -t -m "resultset/row/field[#name='c']" -v . --nl file.xml

I think -n or --nl does the trick to exit with 0 when there is empty match.
Together with -m ... -v . seems to do what OP wants.
# Empty match
$ echo "<field name=\"c\"></field>" | xmlstarlet sel -t -m "field[#name='c']" -v . --nl
$ echo "${PIPESTATUS[#]}"
0 0
# No match
$ echo "<field name=\"c\"></field>" | xmlstarlet sel -t -m "field[#name='non_existent']" -v . --nl
$ echo "${PIPESTATUS[#]}"
0 1
# Non-empty match
$ echo "<field name=\"c\">xyz</field>" | xmlstarlet sel -t -m "field[#name='c']" -v . --nl
xyz
$ echo "${PIPESTATUS[#]}"
0 0
Notes
This outputs non-empty string. For me it is not a problem, as I filter results later anyway.

Related

check last character of command output

#!/bin/bash
ip="172.16.0.28"
community="abcd"
currentState=$(snmpget -v 1 -Oe -c $community $ip PowerNet-MIB::sPDUOutletCtl.$1)
currentState="${currentState: -1}"
if [ $currentState == 2 ] ; then
snmpset -v 1 -c abcd 172.16.0.28 PowerNet-MIB::sPDUOutletCtl.$1 i outletOn
else
snmpset -v 1 -c abcd 172.16.0.28 PowerNet-MIB::sPDUOutletCtl.$1 i outletOff
fi
Is it possible to rewrite the if statement as an one liner?
Something like
echo "${$(snmpget -v 1 -Oe -c abcd 172.16.0.28 PowerNet-MIB::sPDUOutletCtl.1): -1}"
bash: ${$(snmpget -v 1 -Oe -c abcd 172.16.0.28 PowerNet-MIB::sPDUOutletCtl.1): -1}: bad substitution
does not work.
EDIT: As requested, a possible return of snmpget:
$ snmpget -v 1 -Oe -c abcd 172.16.0.28 PowerNet-MIB::sPDUOutletCtl.1
PowerNet-MIB::sPDUOutletCtl.1 = INTEGER: 1
In code compatible with all POSIX-compliant shells:
case "$(snmpget -v 1 -Oe -c "$community" "$ip" "PowerNet-MIB::sPDUOutletCtl.$1")" in
*2) sfx=On;; # output from snmpget ends with 2
*) sfx=Off;; # output from snmpget does not end with 2
esac
You can't use the parameter expansion operators like this, they only operate on parameters (i.e. variables).
You can pipe to the tail command to get the last character of output.
currentState="$(snmpget -v 1 -Oe -c abcd 172.16.0.28 PowerNet-MIB::sPDUOutletCtl.1 | tail -c 2)"
You need to use -c 2 because the last character is a newline, and you want the character before that. The command substitution will then discard the newline at the end of the tail output.
Assumptions:
currentState ends in a character that's expected to be a number (or at least if the last character is 2 then the snmpget call should end with ...On)
the objective is to determine if the last snmpset arg should end with ..On or ..Off
One idea:
ip="172.16.0.28"
community="abcd"
currentState=$(snmpget -v 1 -Oe -c $community $ip PowerNet-MIB::sPDUOutletCtl.$1)
currentState="${currentState: -1}"
# figure out the suffix ...
sfx='Off'
[[ "${currentState}" = "2" ]] && sfx='On'
# now make the snmpset call
snmpset -v 1 -c "${community}" "${ip}" PowerNet-MIB::sPDUOutletCtl.$1 i outlet"${sfx}"

Use XMLStarlet to insert a single value too long to fit on a command line

Suppose I have an xml file:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="a"></string>
</map>
And I want to set the value of string with attribute a with something big:
$ xmlstarlet ed -u '/map/string[#name="a"]' -v $(for ((i=0;i<200000;i++)); do echo -n a; done) example.xml > o.xml
This will result in bash error "Argument list is too long". I was unable to find option in xmlstarlet which accept result from a file. So, how would I set the value of xml tag with 200KB data+?
Solution
After trying to feed chunks into the xmlstarlet by argument -a (append), I realized that I am having additional difficulties like escape of special characters and the order in which xmlstarlet accepts these chunks.
Eventually I reverted to simpler tools like xml2/sed/2xml. I am dropping the code as a separate post below.
This, as a workaround for your own example that bombs because of the ARG_MAX limit:
#!/bin/bash
# (remove 'echo' commands and quotes around '>' characters when it looks good)
echo xmlstarlet ed -u '/map/string[#name="a"]' -v '' example.xml '>' o.xml
for ((i = 0; i < 100; i++))
do
echo xmlstarlet ed -u '/map/string[#name="a"]' -a -v $(for ((i=0;i<2000;i++)); do echo -n a; done) example.xml '>>' o.xml
done
SOLUTION
I am not proud of it, but at least it works.
a.xml - what was proposed as an example in the starting post
source.txt - what has to be inserted into a.xml as xml tag
b.xml - output
#!/usr/bin/env bash
ixml="a.xml"
oxml="b.xml"
s="source.txt"
echo "$ixml --> $oxml"
t="$ixml.xml2"
t2="$ixml.xml2.edited"
t3="$ixml.2xml"
# Convert xml into simple string representation
cat "$ixml" | xml2 > "$t"
# Get the string number of xml tag of interest, increment it by one and delete everything after it
# For this to work, the tag of interest should be at the very end of xml file
cat "$t" | grep -n -E 'string.*name=.*a' | cut -f1 -d: | xargs -I{} echo "{}+1" | bc | xargs -I{} sed '{},$d' "$t" > "$t2"
# Rebuild the deleted end of the xml2-file with the escaped content of s-file and convert everything back to xml
# * The apostrophe escape is necessary for apk xml files
sed "s:':\\\':g" "$s" | sed -e 's:^:/map/string=:' >> "$t2"
cat "$t2" | 2xml > "$t3"
# Make xml more readable
xmllint --pretty 1 --encode utf-8 "$t3" > "$oxml"
# Delete temporary files
rm -f "$t"
rm -f "$t2"
rm -f "$t3"

How to read string to an array and get values from the array

Currently I have
DATE_LIST=$(cat "$OUT_FILE" | xmlstarlet sel -T -t -m "//*[local-name()='entry']//*[local-name()='$start_position_date'][#name='beginposition']" -v '.' -n)
The result is something like:
DATE_LIST= 2015-10-10
2015-11-11
... and so on
IFS='\n' read -a array <<< "$DATE_LIST"
echo "${array[0]}" //I get the first one
echo "${array[1]}" //I get nothing
How to parse it correctly? DATE_LIST is generated from xml and strings are separated with \n.
This appends each line from the output into an array, supporting lines with
withespaces.
array=()
IFS='
'
for line in $(cat "$OUT_FILE" | xmlstarlet set -T ...)
do
array+=("$line")
done
unset IFS

xmlstarlet to add exactly one element to a file

I have hundreds of xml files to process - some have a particular desired tag, some don't. If I just add the tag to all files then some files get 2 tags (no surprises there!). How do I do it in xmlstarlet without a clumsy grep to select the files to work on? eg:
I have this in some files:
...
<parent_tag>
<another_tag/> <-- but not in all files
</parent_tag>
I want this (but some files already have it):
...
<parent_tag>
<good_tag>foobar</good_tag>
<another_tag/>
</parent_tag>
eg this works but I wish I could do it entirely in xmlstarlet without the grep:
grep -L good_tag */config.xml | while read i; do
xmlstarlet ed -P -S -s //parent_tag -t elem -n good_tag -v "" $i > tmp || break
\cp tmp $i
done
I got myself tangled up in some XPATH exoticism like:
xmlstarlet sel --text --template --match //parent_tag --match "//parent_tag/node()[not(self::good_tag)]" -f --nl */config.xml
... but it's not doing what I had hoped ...
Just select only <parent_tag/> elements which do not contain a <good_tag/> for inserting:
xmlstarlet ed -P -S -s '//parent_tag[not(good_tag)]' -t elem -n good_tag -v ""
If you also want to test for the right contents of the tag:
xmlstarlet ed -P -S -s '//parent_tag[not(good_tag[.="foobar"])]' -t elem -n good_tag -v ""

bash passing parameters to xml ed

Inside a bash script I want to pass arguments to the xml ed command of the xmlstarlet tools.
Here's the script:
#!/bin/bash
# this variable holds the arguments I want to pass
ED=' -u "/a/#id" -v NEW_ID -u "/a/b" -v NEW_VALUE'
# this variable holds the input xml
IN='
<a id="OLD_ID">
<b>OLD_VALUE</b>
</a>
'
# here I pass the arguments manually
echo $IN | xml ed -u "/a/#id" -v NEW_ID -u "/a/b" -v NEW_VALUE input.xml
# here I pass them using the variable from above
echo $IN | xml ed $ED
Why does the first call work, ie it gives the desired result:
# echo $IN | xml ed -u "/a/#id" -v NEW_ID -u "/a/b" -v NEW_VALUE input.xml
<?xml version="1.0"?>
<a id="NEW_ID">
<b>NEW_VALUE</b>
</a>
While the second call does not work, ie it gives:
# echo $IN | xml ed $ED
<?xml version="1.0"?>
<a id="OLD_ID">
<b>OLD_VALUE</b>
</a>
In bash, it is better to use arrays for lists of options like this. In this case, it doesn't make a difference, since none of the items embedded in ED contain whitespace.
#!/bin/bash
# this variable holds the arguments I want to pass
ED=( -u "/a/#id" -v NEW_ID -u "/a/b" -v NEW_VALUE)
# this variable holds the input xml
IN='
<a id="OLD_ID">
<b>OLD_VALUE</b>
</a>
'
# here I pass the arguments manually
echo $IN | xml ed -u "/a/#id" -v NEW_ID -u "/a/b" -v NEW_VALUE input.xml
# here I pass them using the variable from above
echo $IN | xml ed "${ED[#]}"
Get rid of the double quotes, because they're not processed after expanding variables:
ED=' -u /a/#id -v NEW_ID -u /a/b -v NEW_VALUE'

Resources