Here I only want to share how I was confused (and almost discarded the idea of using invoke) after reading the documentation and how I think it could be improved.
In https://github.com/pyinvoke/invoke/blob/master/sites/docs/getting_started.rst#declaring-pre-tasks we read:
"Let’s expand our docs builder with a new cleanup task that runs before every build (but which, of course, can still be executed on its own):"
from invoke import task, run
run("rm -rf docs/_build")
run("sphinx-build docs docs/_build")
I think one should emphasize the crucial role of the force flag in rm. Without it, running invoke clean followed by invoke build doesn't work and, to me, it wasn't immediately clear why. I even made the situation worse by using hide=true in the clean task (I wasn't interested in knowing that there was nothing to remove). I think it could be a could place to mention the option warn=True as an alternative to using rm -f. This option is mentioned in the FAQ but not in getting started.
Yea, having hide=True is the lynchpin here, that would have hid the stderr from your erroring rm, making it impossible to tell that it's what failed and then prevented build from running.
I'd argue this is less a docs problem and more of an outright UX bug; users shouldn't have to guess at why something didn't behave as expected.
This is something one really easily stumbles over.
I'm not 100 % sure how the error handling of run() works under the hood, I guess it just raises an exception and it bubbles up from there. Imho it should be so that any un-handled run() failure should print both the command line that failed, and it's output (ideally with some nice colors :)
I like to have my scripts quiet by default - but also have them print useful info with little extra effort when things go wrong. Shellscript/make/... make this hard, but Invoke is in a position to make it easy (and the default).
That's a good point, @enkore - thanks. Also I guess good Unix behavior, now that I think about it - be silent unless something bad happened. So I'll definitely consider going with the 2nd bullet point in my above comment, in addition to or instead of the 3rd :)
I think I'm running into a flavor of this, if not exactly this, in the general case - not just with pre-tasks. Seems to be straight up that any Program-driven use (i.e. inv vs library/repl) is exiting correctly but otherwise not making it clear that it's exiting due to UnexpectedExit instead of just falling off the end of the script.
I may have changed how this works in the last year or so, not entirely sure. Need to dig a bit. Think the solution is some hybrid of the 2nd/3rd options in my old comment - don't limit it to pre-tasks, probably, since I think the issue is the same either way (whether inv exits during a pre-task or a main task should be irrelevant). Just straight up handle UnexpectedExit better by inspecting its wrapped execution request to see if hide was set, and verbosely complain if so, on assumption that the user wasn't otherwise redirecting output etc.
EDIT: two related thoughts:
The code in question is here: https://github.com/pyinvoke/invoke/blob/70055e8498bb0ab944acc7d337a73d32c1b85d56/invoke/program.py#L280-L286
It raises one other issue which is how to unify "raise traceback" with "exit with a very specific exit code". If we just raise or raise e, Python will exit with code 1. Now, granted, the "real" exit code is clearly displayed in the value of UnexpectedExit - see below example, where the outer Invoke-running Python process exits 1 but the inner one exits 15:
» python -c "from invoke import Context; Context().run('python -c \'import sys; sys.exit(15)\'', hide=True)"
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "invoke/context.py", line 60, in run
return runner_class(context=self).run(command, **kwargs)
File "invoke/runners.py", line 256, in run
return self._run_body(command, **kwargs)
File "invoke/runners.py", line 395, in _run_body
invoke.exceptions.UnexpectedExit: Encountered a bad command exit code!
Command: "python -c 'import sys; sys.exit(15)'"
Exit code: 15
Suppose I could get both worlds by doing eg traceback.print_stack() or whatever, instead of raising; then just exiting as we currently do.
MORE EDIT: actually I missed the bit 2-3 lines above the linked snippet, where we write out ParseError to sys.stderr. Ought to unify with that, I think.
On the fence re: whether we should even hinge this printing on the value of hide, but I think it's probably still worthwhile given the annoyances in Fabric 1 of "double output" on error - getting both the normally-printed stdout/err of a failing program, and then the same (captured) out/err in the traceback/abort message.
Bit more thought after poking this (& adding some more tests around the existing functionality of UnexpectedExit's __repr__):
Putting all that on top of what I came into this wanting to add (the conditional display of hidden streams on error) makes me think we should unify the two and tweak it a bit:
Change tests for new desired behavior re #349
Display stringified UnexpectedExit in Program.
I double-checked the original case for this ticket (pre-task calling run('rm <nonexistent dir', hide=True)) and the just-merged solution does seem to make it better, i.e. instead of simply quietly exiting 1, you'll see this instead:
run('rm <nonexistent dir', hide=True)
» inv build
Encountered a bad command exit code!
Command: 'rm wat'
Exit code: 1
rm: wat: No such file or directory
Thinking there could be more intelligence (cleverness? =/) added to the display - e.g. for empty stdout, it looks...a bit off. But this is still a big step in the right direction, and future changes would be additive/cosmetic instead of behavioral.
Changelog re #349
The next day: derp! I forgot another minor wrinkle, we still don't want UnexpectedExit.__repr__ to display when no hiding was happening. In that situation, it is annoyingly verbose and unwanted by most users. Fixing now.