mvdan / sh

A shell parser, formatter, and interpreter with bash support; includes shfmt
https://pkg.go.dev/mvdan.cc/sh/v3
BSD 3-Clause "New" or "Revised" License
7.16k stars 339 forks source link

syntax: keep "exit guards" in place #564

Open powerman opened 4 years ago

powerman commented 4 years ago

https://docs.fedoraproject.org/en-US/defensive-coding/programming-languages/Shell/#sect-Defensive_Coding-Shell-Edit_Guard recommends to have main "$@" ; exit $? as single line… Maybe it's worth to support this case and avoid auto-rewriting it as two lines?

powerman commented 4 years ago

BTW, there are other similar workarounds, but they also require two things on same line, e.g.:

{
  # code here
}; exit $?;
mvdan commented 4 years ago

The formatter doesn't try to interpret or understand your code in any way. So we can't treat main "$@"; exit 1 any different than any other pair of commands like foo; bar.

However, you might be interested in https://github.com/mvdan/sh/issues/261, which is a more generic solution we could actually implement. If you configured the formatter to allow two consecutive commands on a single line, then I think your single-liner would work.

powerman commented 4 years ago

Yeah, I think solution for #261 will works here too.

mvdan commented 4 years ago

Thanks! Closing in favor of that one then.

theclapp commented 4 years ago

This is an interesting feature of bash, and an interesting possible-bug because of it.

Wouldn't putting exit at the end of your main() work just fine, though?

function main {
  # ... code ...
  exit $?
}

main

Bash has to read the entirety of the main function to execute it, so there's no way editing the script while it's running could remove it or do something after main exits.

powerman commented 4 years ago

@theclapp I doubt this will work. AFAIK bash updates file seek position after executing each line - to support unpacking shell-archives located at same file. So, I suppose while executing main() it'll do seek() after each line, and thus will be affected by the issue. Two things should be at same line to avoid this - it won't seek() at the middle of the line (I suppose).

theclapp commented 4 years ago

So, I suppose while executing main() it'll do seek() after each line

No, it reads all of main at once, stores it, and then executes it when called. If this wasn't true, then the trick of writing main "$@" ; exit $? wouldn't work anyway, because what if you just updated the definition of main while it was running?

In any case, fwiw, I tried it (GNU bash, version 5.0.16(1)-release (x86_64-apple-darwin18.7.0)), and it does work.

For anyone following along at home: If you want to try to reproduce this, be sure your editor writes files in place. E.g. in Vim set nobackup nowritebackup. Otherwise, when you save, it writes to a new file, and then unlinks the old file and renames the new one on top of it. But bash keeps the file open and reads the old (now deleted) one, so you haven't actually updated the file that bash is actually reading. You can check that it's the exact same file by checking the inode number via ls -li filename.

mvdan commented 4 years ago

I think it was wrong to close this issue as a duplicate of #261; re-reading both again, #261 was mostly about preference or semantics, while this is purely about defensive coding.

Personally, I think @theclapp's solution is the sanest. It works with the current formatting rules - it doesn't require placing two commands in a single line.

If we tried to go the opposite route, and somehow detect some-command; exit some-args, that could get complex really fast. exit is not special syntax, it's just a command like any other. We would fail to detect foo; "exit" $? and cmd=exit; foo; $cmd $?. Similarly, we likely can't predict what the previous command will look like, so I don't know what we could possibly do with echo foo; exit and such.

@powerman thoughts?

mvdan commented 4 years ago

Also, I'm open to ideas on how we could detect the "safe pattern" of having two commands in a single line, without allowing any foo; bar line to stay as a single line.

powerman commented 4 years ago

I don't have personal preferences, any working solution is good for me. But:

mvdan commented 4 years ago

What you say makes sense, but I still don't know how to reliably detect this without turning off line splitting entirely.

powerman commented 4 years ago

I belive we don't have to implement this reliably - best effort (e.g. few regexp) should be good enough and much better than current behaviour anyway.

mvdan commented 4 years ago

So, quite literally, just matching any-command; exit any-arguments and letting that be a single line?

powerman commented 4 years ago

Maybe even ;\s*exit (see above in comments example with code block).

mvdan commented 4 years ago

I would really appreciate if people gave real examples of these multi-command lines used for safety. So far, I've only been shown some-command; exit $?, so that would lead me to think that we should only worry about that pattern.

Are there others?

skyzyx commented 3 years ago

Here's my snippet:

if [ ! -f files/SHA256SUMS ]; then echo "files/SHA256SUMS file missing." 1>&2; exit 1; fi

shfmt formats this to:

if [ ! -f files/SHA256SUMS ]; then
    echo "files/SHA256SUMS file missing." 1>&2
    exit 1
fi

My co-worker hates this formatting for certain validation rules. I don't give a shit. I'm looking to see if there is a way to annotate a file/line to be left alone. This would allow us to use the good parts of shfmt and shellcheck without causing a fight on the team over something I don't care enough about to fight over.

Trying to find a solution, I got bounced from #564 → #261 → #564, and I still can't tell if there's a solution to my issue or not.

Is there a straight answer I've missed?

mvdan commented 3 years ago

There is no support for annotations or to turn off the formatting for parts of a script, no. There are no plans for that either.

This issue will likely get fixed, because exit guards do serve a purpose. Then, your original line would likely format as:

if [ ! -f files/SHA256SUMS ]; then
    echo "files/SHA256SUMS file missing." 1>&2; exit 1
fi
kaey commented 3 years ago

This specific code can be rewritten as

die() {
    echo "$*" >&2
    exit 1
}

try() {
    "$@" || die "failed: $*"
}

try test -f files/SHA256SUMS
or
test -f files/SHA256SUMS || die "files/SHA256SUMS is missing"

This issue is about guarding against in-place edits while script is running.

skyzyx commented 3 years ago

There is no support for annotations or to turn off the formatting for parts of a script, no. There are no plans for that either.

This issue will likely get fixed, because exit guards do serve a purpose. Then, your original line would likely format as:

if [ ! -f files/SHA256SUMS ]; then
    echo "files/SHA256SUMS file missing." 1>&2; exit 1
fi

That's unfortunate. Guess I'll need to roll up my sleeves and get in a fight.

Thanks.