Catch UCI errors and apply them to custom error message in shell script

I'm in the process of making a very large UCI defaults script which completely automates the setup of my main WRT1900ACSv2 router from start to finish. I'm hoping to share it on the Wiki once it's complete as it may aid people trying to get their head around UCI commands and I think lots of people would find it useful for getting their router up and running quickly from factory resets/custom firmware images etc.

The reason I'm asking for help is because I've styled the script using ANSI escape codes and echo commands which details what's happening in the script. My Unix knowledge is basic so I'm hoping someone can offer some help so that can finish the script and add ideas etc.

By default I hide all output from commands by redirecting stdout and stderr file descriptors to /dev/null and then redirecting a new file descriptor 3 to stdout. It was my lazy way of not having to put &>/dev/null at the end of every line (there's over 1000 lines!)

# Hide all output by default
exec 3>&1 &>/dev/null

For an echo to show in the script output (stdout) I use >&3 at the end:

# Set default radio settings
echo -n "Configuring default radio settings... " >&3
uci batch <<-"EOF"
set wireless.radio0.beacon_int='400'
set wireless.radio0.txpower='23'
set wireless.radio0.legacy_rates='0'
set wireless.radio0.country='GB'
set wireless.radio0.noscan='1'
set wireless.radio0.channel='36'
set wireless.radio1.txpower='20'
set wireless.radio1.noscan='1'
set wireless.radio1.channel='1'
set wireless.radio1.beacon_int='200'
set wireless.radio1.legacy_rates='1'
set wireless.radio1.htmode='HT40'
set wireless.radio1.country='GB'
EOF
echo -e "Done" >&3

As shown in the example above you can see I enclose the echo commands around the UCI commands themselves. The problem I have is the script will always respond as "Done" no matter if the command has completed or not. For example, if I run the SQM UCI command on my stock OpenWrt firmware, the luci-sqm-app package is not installed by default therefore I will receive an error message when I run the command:

$ echo "SQM"
$ # Delete default SQM instance
$ echo -en "\tDeleting default SQM instance... "
$ uci del sqm.eth1
$ echo "Done"
uci: Entry not found

I've done some reading on the trap command but unfortunately it has gone straight over my head. From what I've seen it can capture error messages, print out precisely what line the error is at and print out a custom error message.

What I'm looking to do is place the trap command at the bottom of the UCI commands in place of the simple "Done" response and pickup whether an error can occurred since the first echo command (top of the UCI commands section). From here I would then use an IF ELSE statement to display success or error message dependent on the result.

For perspective here's a snippet of the script including the ANSI colour codes:

#!/bin/ash
#=======================================#
# Clear screen
clear
#=======================================#
# Hide all output by default
exec 3>&1 &>/dev/null
#=======================================#
# Define foreground and background colours
RED_FG="\033[0;31m" >&3
GREEN_FG="\033[0;32m" >&3
WHITE_FG="\033[1;97m" >&3 # Has bold styling
BLUE_BG="\033[44m" >&3
LIGHT_BLUE_BG="\033[104m" >&3
RC="\033[0;39m\033[0;49m" >&3 # Resets colour and style

# Define section styling
Section="${BLUE_BG}${WHITE_FG}" >&3
Config="${LIGHT_BLUE_BG}${WHITE_FG}" >&3
Success="${GREEN_FG}" >&3
Warning="${RED_FG}" >&3
#=======================================#

########################################
########## Clear/Set Defaults ##########
########################################

echo -e "${Section}# Clear/set defaults #${RC}" >&3
#==============================#
#---------- Wireless ----------#
#==============================#
echo -e "\t${Config}Wireless${RC}" >&3

# Set default radio settings
echo -en "\t> Configuring default radio settings... " >&3
uci batch <<-"EOF"
set wireless.radio0.beacon_int='400'
set wireless.radio0.txpower='23'
set wireless.radio0.legacy_rates='0'
set wireless.radio0.country='GB'
set wireless.radio0.noscan='1'
set wireless.radio0.channel='36'
set wireless.radio1.txpower='20'
set wireless.radio1.noscan='1'
set wireless.radio1.channel='1'
set wireless.radio1.beacon_int='200'
set wireless.radio1.legacy_rates='0'
set wireless.radio1.htmode='HT40'
set wireless.radio1.country='GB'
EOF

echo -e "${Success}Done${RC}" >&3
#============================#
#---------- System ----------#
#============================#
echo -e "\t${Config}System${RC}" >&3
# Set system settings
echo -en "\t> Configuring system settings... " >&3
uci batch <<-"EOF"
set system.@system[0].hostname='OpenWrt-AP1'
set system.@system[0].zonename='Europe/London'
set system.ntp.enable_server='1'
EOF
echo -e "${Success}Done${RC}" >&3

This will be useful for those who run this script or a variation of it and they forget to install a package and so a simple message like "Sorry this package doesn't seem be installed" could be displayed.

N.B Two things that spring to mind are, resetting the trap command so that it doesn't capture the entire script and display an error message for a different section of the script other than for the section it's supposed to be displaying for. Secondly I would have to allow stderr to come back through somewhere so the trap command can see it

2>&3

An example output could look like this:
Desired Result

Any ideas?

Welcome to the world of uci-defaults scripts. :wink:

Or you can check if the relevant config file exists and not zero:

if [ -s "/etc/config/sqm" ]; then
uci batch commands...
else
echo "/etc/config/sqm not found!" >&3
fi

You can also grep/parse the /usr/lib/opkg/status to check if package is installed or not. Also, if it's a large script, I suggest you use shellcheck or at least sh -n <scriptname> to check for errors before/as part of your build script.

2 Likes

there are probably 1000+1 ways to structure your code... but once you have a grasp of the basics...

if your desire is for 'section' based actions/status/output... you'd really be better of with some form of cmdwrapper's and/or functions per section/action...

running filehandle redirects inline throughout the script is text heavy, will use alot of repeat logic and be hard to adjust things of you change some core logic...

some good examples for you to check are;

  • @stangri 's vpn-policy (nice coloring examples)
  • sqm's cmdwrapper (tricky function wrapping, cool cmd history / replay function)
  • @dibdot 's services(banip to start with) (nice lib.sh structure)

you could possibly also check my builds rootfs > /etc/rc.custom which essentially mimicks uci-defaults via rc.local using seperate files... makes medium/long term manageability much easier...

2 Likes

I was wondering if there is a way to pickup exit code 1 or literally the words "uci: Entry not found" displayed in the shell output and display an echo command from that? This logical conditions would be placed at the end of the section.

echo -n "Configuring SQM... "
# Delete default SQM instance
echo -en "\tDeleting default SQM instance... "
uci del sqm.eth1
if [stderr = "uci: Entry not found" || exit code = 1 ]; then
    echo -e "SQM is not installed!"
else
    echo "Done"
fi

This is a vague question but, can any command be used within if else conditions to probe/validate whether something returns true or false (1 or 0)? For example I know cat, sed and awk can be used to pull the contents from within a file. What are your favourite validating commands?

Have you got links to these examples you're referring to please?

I'll be totally honest, I would consider my understanding of Unix to be basic but I've found the need to write scripts to help automate everything I do with computers as I get fed up with doing everything manually over and over again. This is why I've delved into Powershell and Unix because it's powerful and can help automate many things, and in bulk. Best of all it can be copied, slightly modified and used elsewhere for a different purpose.

I can write basic scripts that do their job and are mostly unattended, but over the last year or so I've been trying to make my scripts look more visually appealing by using echo commands to show the user running the script what the script is doing, giving the user prompts using if else statements whether to continue or cancel the script for example and using file descriptors to hide irrelevant information that's outputted from certain commands, and instead simply display "Done" or "Error has been found" (or some other custom message) which is what I'm currently trying to do.

I was speculating whether to use a simple if else statement as I thought there would be a smarter way of picking up an error message and rewriting it. Would you say the if else statement way is the best way of approaching output error messages or are you familiar with the trap command or something similar?

Config scripts are supposed to run non-interactively triggered by UCI defaults.
Typically, it should be just a dumb script, no need to make it overly smart, interactive or fancy.

It's also best to avoid the unnecessary I/O-redirection.
When you actually need troubleshooting, invoke the script like this:

sh -e -x -v script.sh

Splitting a huge mass of code to functions can also be helpful as already mentioned above.

1 Like

I'd say it depends on what kind of error you want to process. I've used trap in the simple-adblock package to clean up the temporary files if the process crashes for some unforeseable reason. If you know an error is possible (as in uci entry not founds), I'd use if-else.

If you have found a command which doesn't set its return status as expected, you can capture its full output like this: res="$(cmd param1 param2)" and then parse it.

2 Likes

stick with whatever your are comfortable with...

as above, uci defaults are non-interactive... so there are a few tricks to polishing them or getting feedback...

  • one is the filehandle redirect you've shown
  • one is a console and sending output to it... i.e.;
echo "settimezone... $(uci command)" > /dev/kmsg
  • another one might be to leave it in place with something like
#!/bin/sh

uci -q do this 2>/tmp/uci-default.err.$file || RETVAL=1

exit ${RETVAL:-0}

not perfect by any means... but the idea is to exit with a status of one... so the file will stay in place and you know it has errors... and you'll log the stderr too...

1 Like

The only reason I'm making fancy scripts is because I compiled my own firmware and wanted to test the uci-defaults script before baking it into the firmware. If I was to mess something I could end up with a loop of errors.

@stangri I'll think I'll stick with if else conditions :slight_smile:
@anon50098793 I'll have a play around with your suggestions and post.

Once the script is done and I've removed personal data from it I'll share on the Wiki hopefully.