7

I have a dash shell script that is intended to run to the end of file:

#!/usr/bin/dash
# File b_shell
. a_nonexistent_file
echo "I want to print this line despite any previous error."

However, it stopped running after failure to source a nonexistent file:

$ ./b_shell
./b_shell: 3: .: a_nonexistent_file: not found

I've tried candidate solutions including set -e and || true, which can be easily found on the Internet, but to no avail.

Additional remarks:

  1. I am not allowed to use other shells such as Bash, tcsh, etc. Please don't ask me to switch.
  2. I noticed some other "Not Found" errors won't stop the script, as is illustrated below.
#!/usr/bin/dash
# File b_shell_ok
a_nonexistent_command
ls a_nonexistent_file
echo "This line is printed despite any previous error."
$ ./b_shell_ok
./b_shell_ok: 3: a_nonexistent_command: not found
ls: cannot access 'a_nonexistent_file': No such file or directory
This line is printed despite any previous error.
0

2 Answers 2

9

That's because . is a special builtin. That is a POSIX requirement. Most other POSIX shells also do it, but in the case of bash and zsh only when they're in their respective posix/sh/ksh mode (including when they're invoked as sh).

To remove the specialness of a special builtin, the POSIX way is to prefix it with command:

$ dash -c 'command . /x; echo here'
dash: 1: .: cannot open /x: No such file
here

That won't prevent the shell from exiting if the sourced file calls exit or runs into fatal errors when interpreting its contents. In the case of dash (contrary to bash -o posix for instance), failing special builtins in the sourced file don't cause the shell to exit though as if they were also prefixed with command.

Beware that if the errexit option is also enabled, the shell will still exit if the file to source can't be opened, but this time because command returns with a non-zero exit status:

$ dash -o errexit -c 'command . /x; echo "$? here"'
dash: 1: .: cannot open /x: No such file

The work around is to handle the error:

$ dash -o errexit -c 'command . /x || echo "that is fine"; echo "$? here"'
dash: 1: .: cannot open /x: No such file
that is fine
0 here

Or:

$ dash -o errexit -c 'command . /x && : non-fatal; echo "$? here"'
dash: 1: .: cannot open /x: No such file
2 here

(Or calling command . in the condition part of a if/while/until statement).

In the case of dash (current versions at least, contrary to bash -o posix for instance), that doesn't cancel the effect of errexit for the evaluation of code in the sourced file:

$ dash -ec 'command . ./file || echo fine; echo here'
before
$ bash -o posix -ec 'command . ./file || echo fine; echo x'
before
after
x

One of the many reasons errexit is best avoided except maybe for the simplest of scripts.

Beware that zsh's command predates POSIX' one and has different semantics. You can't use command . there if not in sh emulation / POSIX mode.

To avoid the error, rather than trying and do a check beforehand which comes with TOCTOU race, you could open the file on some fd whilst stderr is discarded and source the corresponding /dev/fd/n file:

$ dash -c '{ command . /dev/fd/3; } 4>&2 2> /dev/null 3< file 2>&4 4>&-; echo here'
here

When a redirection fails, the redirected command (here a command group) is not executed, so the shell doesn't attempt to execute that special builtin, avoiding the side effect of exiting the shell. We still add a command prefix in case the OS is Linux or Cygwin where opening /dev/fd/x is not like a dup(x) and may still fail if the permissions of the file have changed since it was opened on fd 3.

1
  • I didn't expect the problem to be this complex. Not exactly what I wish for (e.g., a switch that can turn off all forced exits in dash without modifying my original commands) but thanks anyway for your thoughtful answer.
    – zanetu
    Mar 27, 2023 at 8:51
8

It's a POSIX requirement that a such a failure must abort the shell:

If no readable file is found, a non-interactive shell shall abort

Therefore your only option is to protect the command by ensuring the file exists and is readable when you need it;

[ -f file ] && [ -r file ] && . ./file

You could also extend this to a full if statement to allow for an error or warning message to be printed if the file didn't exist

5
  • There is nothing to prevent your script reporting the issue when it is detected, if not having the file is acceptable. Mar 25, 2023 at 8:38
  • @StéphaneChazelas add && [ -r file ] after the existence test? Create a function which tests for both so that you don't keep repeating the same code?
    – RonJohn
    Mar 25, 2023 at 11:55
  • @RonJohn, an existence test would be more like [ -e file ] || [ -L file ], [ -f file ] is a check for type (regular after symlink resolution). In any case, that's a TOCTOU type of race. Mar 25, 2023 at 12:00
  • I forgot to mention that I perfer having all the original error messages printed to avoiding them altogether. But according to the link you provided, it seems impossible to have the original error message printed while preventing the exit. Thanks anyway for your informative answer, which explains the root cause of the problem.
    – zanetu
    Mar 27, 2023 at 8:43
  • @zanetu you can synthesise an error message using the if construct I've suggested Mar 27, 2023 at 9:23

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .