50

How can I reduce the following bash script?

grep -P "STATUS: (?!Perfect)" recess.txt && exit 1
exit 0

It seems like I should be able to do it with a single command, but I have a total of 3 here.

My program should:

  • Read recess.txt
  • Exit 1 (or non-zero) if it contains a line with "STATUS: " of NOT "Perfect"
  • Exit 0 if no such line exists (i.e. all "STATUS: " lines are "Perfect")

The answer award goes to the tightest script. Thanks!

Example files

Program should have exit status 0 for this file:

FILE: styles.css 
STATUS: Perfect!

FILE: contour-styles.css
STATUS: Perfect!

Program should have exit status 1 (or non-zero) for this file:

FILE: styles.css 
STATUS: Perfect!

FILE: contour-styles.css
STATUS: Busted 
FAILURES: 1 failure

Id's should not be styled
       1. #asdf
Sean Adkinson
  • 8,425
  • 3
  • 45
  • 64

12 Answers12

64

Just negate the return value.

! grep -P "STATUS: (?!Perfect)" recess.txt
Tgr
  • 27,442
  • 12
  • 81
  • 118
  • 2
    Neat. Short. Elegant. Is there a way to make this work when piping into grep? – wedi Jun 08 '20 at 09:46
  • 9
    @wedi `cat recess.txt | ( ! grep -P "STATUS: (?!Perfect)" )` would work (it runs the grep in a [subshell](https://www.tldp.org/LDP/abs/html/subshells.html)). The trick in cincodenada's answer would work too. – Tgr Jun 09 '20 at 00:26
  • what if grep returns 2 - error occurs. My guess failing would be preferable outcome – feech Jun 10 '20 at 15:43
  • @feech depends on the use case. grep writes to the standard error when an error happens, that might be the preferable way of detecting it. Otherwise you can check the status directly as in cincodenada's answer. – Tgr Jun 10 '20 at 16:49
  • I can't get this to work in Ansible, not even with single quotes! – xjcl Jul 23 '20 at 21:57
  • Slow clap... well played! – Joshua Pinter Aug 19 '20 at 15:25
25

I came across this, needing an onlyif statement for Puppet. As such, Tgr's bash solution wouldn't work, and I didn't want to expand the complexity as in Christopher Neylan's answer.

I ended up using a version inspired by Henri Schomäcker's answer, but notably simplified:

grep -P "STATUS: (?!Perfect)" recess.txt; test $? -eq 1

Which very simply inverts the exit code, returning success only if the text is not found:

  • If grep returns 0 (match found), test 0 -eq 1 will return 1.
  • If grep returns 1 (no match found), test 1 -eq 1 will return 0.
  • If grep returns 2 (error), test 2 -eq 1 will return 1.

Which is exactly what I wanted: return 0 if no match is found, and 1 otherwise.

Community
  • 1
  • 1
cincodenada
  • 2,877
  • 25
  • 35
11

if anyone gets here looking for a bash return code manipulation:

(grep <search> <files> || exit 0 && exit 123;)

this will return 0 (success) when grep finds nothing, and return 123 (failure) when it does. The parenthesis are in case anyone test it as is on the shell prompt. with parenthesis it will not logout on the exit, but just exit the subshell with the same error code.

i use it for a quick syntax check on js files:

find src/js/ -name \*js -exec node \{\} \; 2>&1 | grep -B 5 SyntaxError || exit 0 && exit 1;
gcb
  • 13,901
  • 7
  • 67
  • 92
10

To make it work with set -e surround it in a sub-shell with ( and ):

$ cat test.sh 
#!/bin/bash

set -ex
(! ls /tmp/dne)
echo Success
$ ./test.sh 
+ ls /tmp/dne
ls: cannot access /tmp/dne: No such file or directory
+ echo Success
Success
$ mkdir /tmp/dne
$ ./test.sh 
+ ls /tmp/dne
$ 
Robpol86
  • 1,594
  • 21
  • 18
7

Just negating the return value doesn't work in a set -e context. But you can do:

! grep -P "STATUS: (?!Perfect)" recess.txt || false
Eric Woodruff
  • 6,380
  • 3
  • 36
  • 33
5

You actually don't need to use exit at all. Logically, no matter what the result of grep, your script is going to exit anyway. Since the exit value of a shell script is the exit code of the last command that was run, just have grep run as the last command, using the -v option to invert the match to correct the exit value. Thus, your script can reduce to just:

grep -vqP "STATUS: (?!Perfect)" recess.txt

EDIT:

Sorry, the above does not work when there are other types of lines in the file. In the interest of avoiding running multiple commands though, awk can accomplish the entire shebang with something like:

awk '/STATUS: / && ! /Perfect/{exit 1}' recess.txt

If you decide you want the output that grep would have provided, you can do:

awk '/^STATUS: / && ! /Perfect/{print;ec=1} END{exit ec}' recess.txt
Christopher Neylan
  • 8,018
  • 3
  • 38
  • 51
  • 1
    Yeah, that was one of the first things I tried, but this question is subtly harder than that. You see, by using "-v", it just returns every line of the file just about, which is always going to give me an exit status of "0" in grep if anything is returned. So unfortunately this doesn't quite meet the requirements of my program. – Sean Adkinson Mar 12 '13 at 17:37
  • that's actually why i added `-q`--your list of requirements never listed that you cared about the output of `grep` :) – Christopher Neylan Mar 12 '13 at 17:39
  • Correct, I don't care about output, but for my broken file (added to question) the exit code is still "0" using this command – Sean Adkinson Mar 12 '13 at 17:44
  • 1
    ah, yes. the `-v` is catching more than it should. i've updated with an alternative to grep, if your main goal is avoiding running multiple commands. – Christopher Neylan Mar 12 '13 at 18:07
  • Nice! Yes, my main goal is to do this seemingly simple task in a nice tight piece of code. Changed to upvote. Thanks! I'll leave open for the moment, though, to see if anyone answers the exact title of the question, which specifically asks about grep. – Sean Adkinson Mar 12 '13 at 18:11
  • edit: the END block is actually only needed if you want the output. if you don't care about the output, you can just exit 1 as soon as you find a bad status line. p.s., fun question – Christopher Neylan Mar 12 '13 at 18:16
  • @SeanAdkinson: Whilst I agree that the above satisfies the "single-line" requirement; it's not clear that's a particularly useful requirement! My answer could equally be done in a single line by adding a semi-colon... – Oliver Charlesworth Mar 12 '13 at 18:31
  • It seems `-P` is missing on OSX. – kenorb Apr 02 '14 at 09:32
1

Use the special ? variable:

grep -P "STATUS: (?!Perfect)" recess.txt
exit $((1-$?))

(But note that grep may also return 2, so it's not clear what you'd want to occur in such cases.)

Oliver Charlesworth
  • 267,707
  • 33
  • 569
  • 680
  • I agree with your comment above - one-liner isn't particular useful. I'm really just looking for a slicker way to do this thing that seems like it should be easy. I did say in the question that I would award it to the tightest code. You have a cool trick I didn't know about, but it seems a little fragile if grep can return a status of 2 (which I didn't know). Thanks though! – Sean Adkinson Mar 12 '13 at 19:11
  • @SeanAdkinson: It's ok, I wasn't fighting for upvotes, just curious what the goal was here ;) – Oliver Charlesworth Mar 12 '13 at 19:13
  • To fix the issue with `grep` returning exit code 2 on error, you can do `exit $(($?^1))`, which only flips the lowest bit. This preserves the error bit for 2, which I think is what you'd normally want. – Jaap Eldering Feb 20 '14 at 16:32
1

The problem with the grep answers is that if the file is empty you also get a clean response, as if the file had a perfect. So personally I gave up on grep for this and used awk.

awk 'BEGIN{ef=2}; /STATUS: Perfect/{ ef=0;}; /STATUS: Busted/{ print;eff=3;}; END{exit (ef+eff)}' a.txt ; echo $?

This has exit status:
 0 :  Perfect and !Busted
 2 : !Perfect and  Busted
 3 :  Perfect and  Busted
 5 : !Perfect and !Busted
M Hutson
  • 113
  • 7
0
[ $(grep -c -P "STATUS: (?!Perfect)" recess.txt) -eq 0 ]
Chris
  • 632
  • 4
  • 11
  • 18
0

I also needed such a solution for writing puppet only if statements and came up with the following command:

/bin/grep --quiet 'root: root@ourmasterdomain.de' /etc/aliases; if [ $? -eq 0 ]; then test 1 -eq 2; else test 1 -eq 1; fi;

DevelopmentIsMyPassion
  • 3,541
  • 4
  • 34
  • 60
0

Since someone already posted a Puppet solution, I might as well add how to invert a shell command run by Ansible:

  - name: Check logs for errors
    command: grep ERROR /var/log/cassandra/system.log
    register: log_errors
    failed_when: "log_errors.rc == 0"

I.e. you just set the failed condition to the return code being 0. So this command fails if we do find the word ERROR in our logs.

I chose this rather than grep -v as that also inverts grep's output, so we would receive all DEBUG/INFO/WARN lines in log_errors.stdout_lines which we do not want.

xjcl
  • 12,848
  • 6
  • 67
  • 89
0

Node users might want to use a Windows-compatible solution. Here's a NodeJS script which does it:

const { spawnSync } = require('child_process')
const [_node, _thisFile, command, ...args] = process.argv
const subprocess = spawnSync(command, args, { stdio: 'inherit' })
process.exit(subprocess.status === 0 ? 1 : 0)

See the following gist for the full version of the script: https://gist.github.com/mathieucaroff/39a721d6e3959c0a51e035c984c61ac9

Mathieu CAROFF
  • 1,230
  • 13
  • 19