I have a bunch of shell scripts which incorrectly assume /bin/sh to be equivalent to /bin/bash. E.g., they have the #!/bin/sh shebang, but use the source command instead of . (dot).
I run Ubuntu 16, where /bin/sh links to dash, and thus bash-isms are not supported.
I need to run the scripts periodically. Also, from time to time I will need to update them from the original author, who is not into fixing this particular bug.
I would like to avoid fixing all these files (there are a bunch of them, they are not mine, and I’ll loose all the changes after update). Also, I would like to avoid making global changes to system, since it might potentially break other scripts.
Is there a way to somehow create a (temporary or not) environment with /bin/sh pointing to bash, to be used for these scripts, while not touching the global system /bin/sh?
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
I suppose mount namespaces or such could be used to arrange for different processes/users to have a different idea of what /bin/sh is.
But that sounds hackish, and could also count as “making permanent changes to the system”. It would probably be easier to just make that one-line fix. Make that fix part of your update process, and post a bug report and a patch about the wrong hashbang upstream.
With GNU sed, something like this should do to fix them:
sed -i -e '1s,^#! */bin/sh,#!/bin/bash,' /all/the/scripts/*
Method 2
If /bin/sh -> /bin/dash is a dynamically linked executable on your system as on mine (you can check that with file(1)), you can use a LD_PRELOAD hack for that.
It works like this: A small dynamic library loaded with LD_PRELOAD overrides the glibc’s __libc_start_main (the function that calls the executable’s main() function), and if argv[0] == /bin/sh then it exec’s /bin/bash instead with the same arguments (except for argv[0]); otherwise it calls through to the original __libc_start_main as if nothing had happened.
$ cat sh_is_bash.c
#define _GNU_SOURCE /* for RTLD_NEXT */
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>
#include <err.h>
int __libc_start_main(
int (*main)(int,char**,char**), int ac, char **av,
int (*init)(int,char**,char**), void (*fini)(void),
void (*rtld_fini)(void), void *stack_end)
{
typeof(__libc_start_main) *real_lsm;
if(ac > 0 && !strcmp(av[0], "/bin/sh")){
av[0] = "/bin/bash";
execv(av[0], av);
err(1, "execv %s", av[0]);
}else if(real_lsm = dlsym(RTLD_NEXT, "__libc_start_main"))
return real_lsm(main, ac, av, init, fini, rtld_fini, stack_end);
else
errx(1, "BUG: dlsym: %s", dlerror());
}
$ cc -fPIC -shared -Wall -W -Wno-parentheses sh_is_bash.c -o sh_is_bash.so -ldl
$ LD_PRELOAD=`pwd`/sh_is_bash.so program ...
Any script with the #! /bin/sh shebang will be executed with /bin/bash instead of /bin/sh when the LD_PRELOAD environment variable contains the absolute path of sh_is_bash.so.
This is ugly, but it does not require any hard changes either to your system or to the scripts, it’s easy to deploy & manage, and it doesn’t need any special privileges for that.
Method 3
You can easily fix them, don’t break your system!
find . -name '*.sh' -type f -exec sed -i '1s|^#! */bin/sh|#!/bin/bash|' {} +
Method 4
I’ve managed to achieve somewhat close to what I initially wanted by using mount namespaces. (My original solution used unionfs as well, but as it turned out it’s not needed at all). It is used to bind-mount /bin/bash to /bin/sh for a limited set of processes. The short procedure to set up a new shell, where sh is bash, is described below.
First we start new shell with an isolated mount namespace:
sudo unshare -m sudo -u user /bin/bash
And then in the new shell we bind-mount /bin/bash to /bin/sh:
sudo mount --bind /bin/bash /bin/sh
That’s it!
Let’s see what we’ve got in this shell:
<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="3b4e485e497b4e594e554f4e">[email protected]</a>:~$ /bin/sh --version GNU bash, version ... <a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="2356504651635641564d5756">[email protected]</a>:~$ diff -s /bin/sh /bin/bash Files /bin/sh and /bin/bash are identical
But if running in another shell:
<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="285d5b4d5a685d4a5d465c5d">[email protected]</a>:~$ /bin/sh --version /bin/sh: 0: Illegal option -- <a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="0376706671437661766d7776">[email protected]</a>:~$ diff -s /bin/sh /bin/bash Binary files /bin/sh and /bin/bash differ <a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="3e4b4d5b4c7e4b5c4b504a4b">[email protected]</a>:~$ diff -s /bin/sh /bin/dash Files /bin/sh and /bin/dash are identical
Method 5
Scripts that contain non-POSIX extensions should have the related correct #! header, in your case:
#!/bin/bash
so I see no way from editing all incorrect scripts.
BTW: if you are really sure, you could create a temporary link to bash and rename it:
cd /bin ln -s bash nsh mv nsh sh
Since mv works atomically, this will make sure that there always is a working /bin/sh
As a result, scripts and other shells currently running while you do the rename would continue to work and after the rename, this would call bash instead of dash.
If your system however runs in a way that allows to edit the scripts, I would rather only edit the scripts.
If you replace /bin/sh with a link to bash, do not forget to fix this to make /bin/sh be a link to dash again after you are done.
If the scripts in question are part of a binary packet, don’t forget to make a bug report against that problem to your upstream.
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0