6

I have a lot of bash commands.Some of them fail for different reasons. I want to check if some of my errors contain a substring.

Here's an example:

#!/bin/bash

if [[ $(cp nosuchfile /foobar) =~ "No such file" ]]; then
    echo "File does not exist. Please check your files and try again."
else
    echo "No match"
fi

When I run it, the error is printed to screen and I get "No match":

$ ./myscript
cp: cannot stat 'nosuchfile': No such file or directory
No match

Instead, I wanted the error to be captured and match my condition:

$ ./myscript
File does not exist. Please check your files and try again.

How do I correctly match against the error message?

P.S. I've found some solution, what do you think about this?

out=`cp file1 file2 2>&1`
if [[ $out =~ "No such file" ]]; then
    echo "File does not exist. Please check your files and try again."
elif [[ $out =~ "omitting directory" ]]; then
    echo "You have specified a directory instead of a file"
fi
codeforester
  • 39,467
  • 16
  • 112
  • 140
smart
  • 1,975
  • 5
  • 26
  • 46
  • Don't quote right part of `=~`. It's a regex. If you use quotes it's only a string. – Cyrus Jul 30 '17 at 21:00
  • Well, how to handle concrete text with a few words then? using `$(command) =~ first\sword\sgoes\shere` or how? – smart Jul 30 '17 at 21:03
  • 1
    Well, it looks like I may use something like: `a=\`command 2>&1\`; [[ $a =~ "substring" ]]` So far, it looks like works fine. – smart Jul 30 '17 at 21:06
  • Could you please update your question with real code, as well as actual and expected output? It's much more helpful that way. – that other guy Jul 30 '17 at 21:07
  • I suggest: `first[[:blank:]]word[[:blank:]]goes[[:blank:]]here` – Cyrus Jul 30 '17 at 21:11
  • @thatotherguy added more information. – smart Jul 30 '17 at 21:14
  • @smart Very nice! Can you also include the actual and expected output? For example, "This is the output I get when I run it: (...). What I expected to happen was: (,...) because (...)" – that other guy Jul 30 '17 at 21:17
  • See `help [[` for the difference between `=~` and `==`. – Cyrus Jul 30 '17 at 21:18
  • @thatotherguy, The exact wording is not important here. I've added enough of needed logic into my examples. If `... not exist...` raises, echo "File does not exist...". If `...symbolic link...` raises, echo "Your file is a ..."` – smart Jul 30 '17 at 21:22
  • @smart See [How to create a Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve) for how and why you should be doing this. It's part of writing a useful and well researched question. – that other guy Jul 30 '17 at 21:30
  • @thatotherguy, it's well-researched question with suggested solution, but maybe somebody knows something better. Come on.. – smart Jul 30 '17 at 21:38
  • @smart I edited it with a MCVE to make it easier for future readers to identify with the problem, and to make it more explicit what the problem is (stderr vs bad matching) and upvoted ^^ – that other guy Jul 30 '17 at 21:47
  • @thatotherguy, I'm not sure that it was correctly translated. – smart Jul 30 '17 at 21:54
  • @smart Do feel free to update it! For example, my `cp` never says `not exist`, it instead says `No such file..`, so on my system, that would have been one of the problems. If your system does say `not exist`, then it's fine. This is why it's important to include actual output. – that other guy Jul 30 '17 at 22:03

3 Answers3

8

I'd do it like this

# Make sure we always get error messages in the same language
# regardless of what the user has specified.
export LC_ALL=C

case $(cp file1 file2 2>&1) in 
    #or use backticks; double quoting the case argument is not necessary
    #but you can do it if you wish
    #(it won't get split or glob-expanded in either case)
    *"No such file"*)
        echo >&2 "File does not exist. Please check your files and try again." 
        ;;
    *"omitting directory"*)
        echo >&2 "You have specified a directory instead of a file"
        ;;
esac

This'll work with any POSIX shell too, which might come in handy if you ever decide to convert your bash scripts to POSIX shell (dash is quite a bit faster than bash).

You need the first 2>&1 redirection because executables normally output information not primarily meant for further machine processing to stderr. You should use the >&2 redirections with the echos because what you're ouputting there fits into that category.

that other guy
  • 116,971
  • 11
  • 170
  • 194
Petr Skocik
  • 58,047
  • 6
  • 95
  • 142
  • 2
    It's probably worth mentioning why `2>&1` helps – that other guy Jul 30 '17 at 21:52
  • Well, is this solution faster or better for some reason than suggested by me? – smart Jul 30 '17 at 21:59
  • @smart Your solution is perfectly fine, only non-portable. Some day you might want to convert to a different (faster?) shell and writing close to POSIX might save you some rewriting. (I recently rewrote a bunch of largish and somewhat performance sensitive bash code to dash, and I'm quite happy with the performance gains.) – Petr Skocik Jul 30 '17 at 22:07
  • If you want to do something for the non-matching case, add `*) { echo "did not match!" };;` right before the `esac` line – Peter Kionga-Kamau May 04 '22 at 17:23
1

PSkocik's answer is the correct one for one you need to check for a specific string in an error message. However, if you came looking for ways to detect errors:

I want to check whether or not a command failed

Check the exit code instead of the error messages:

if cp nosuchfile /foobar
then
  echo "The copy was successful."
else
  ret="$?"
  echo "The copy failed with exit code $ret"
fi

I want to differentiate different kinds of failures

Before looking for substrings, check the exit code documentation for your command. For example, man wget lists:

EXIT STATUS
   Wget may return one of several error codes if it encounters problems.

   0   No problems occurred.
   1   Generic error code.
   2   Parse error---for instance, when parsing command-line options
   3   File I/O error.
   (...)

in which case you can check it directly:

wget "$url"
case "$?" in
    0) echo "No problem!";;
    6) echo "Incorrect password, try again";;
    *) echo "Some other error occurred :(" ;;
esac

Not all commands are this disciplined in their exit status, so you may need to check for substrings instead.

that other guy
  • 116,971
  • 11
  • 170
  • 194
  • Maybe also note that checking for specific strings will fail if the message catalog is localized. You hardly want to hard-code an enumeration of the ways to say `file not found` in dozens or hundreds of languages. – tripleee Jun 02 '18 at 05:05
  • The other answer wisely sets the locale to get around that – that other guy Jun 02 '18 at 13:47
0

Both examples:

out=`cp file1 file2 2>&1`

and

case $(cp file1 file2 2>&1) in 

have the same issue because they mixing the stderr and stdout into one output which can be examined. The problem is when you trying the complex command with interactive output i.e top or ddrescueand you need to preserve stdout untouched and examine only the stderr. To omit this issue you can try this (working only in bash > v4.2!):

shopt -s lastpipe
declare errmsg_variable="errmsg_variable UNSET"

command 3>&1 1>&2 2>&3 | read errmsg_variable

if [[ "$errmsg_variable" == *"substring to find"* ]]; then
    #commands to execute only when error occurs and specific substring find in stderr
fi

Explanation

This line

command 3>&1 1>&2 2>&3 | read errmsg_variable

redirecting stderr to the errmsg_variable (using file descriptors trick and pipe) without mixing with stdout. Normally pipes spawning own sub-processes and after executing command with pipes all assignments are not visible in the main process so examining them in the rest of code can't be effective. To prevent this you have to change standard shell behavior by using:

shopt -s lastpipe

which executes last pipe manipulation in command as in the current process so:

| read errmsg_variable

assignes content "pumped" to pipe (in our case error message) into variable which resides in the main process. Now you can examine this variable in the rest of code to find specific sub-string:

if [[ "$errmsg_variable" == *"substring to find"* ]]; then
    #commands to execute only when error occurs and specific substring find in stderr
fi