0

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?

Alex Che
  • 261
  • 1
    Do not replace /bin/sh! You have no way to know which other programs, scripts and so on rely on /bin/sh (a lot!), and this may seriously break your system. Instead, fix your shell scripts! – mhutter Sep 12 '18 at 00:06
  • Thanks. I was hoping there was a way to create a separate environment and avoid global system changes. I've clarified my question a little. – Alex Che Sep 12 '18 at 07:28
  • @AlexChe : So you have a set of programs with errors in it, and solve the problem by temporarily introducing an even bigger error in your system? If the programs have an incorrect #! line, the maintainers simply have to fix them. – user1934428 Sep 12 '18 at 07:46
  • @user1934428 , I have a set of programs which maintainers are not going to fix. I want to create a an isolated environment to run them, not touching the global /bin/sh. – Alex Che Sep 12 '18 at 10:32
  • I have a set of programs which maintainers are not going to fix. Seriously? Those maintainers actually believe /bin/sh is the same as /bin/bash?!?!?! Who are these, ummm, not-as-smart-as-they-think-they-are maintainers? So I can avoid anything written by them in the future? Tell them to read some of the links in this Google search: https://www.google.com/search?q=sh+is+not+bash – Andrew Henle Sep 12 '18 at 10:54
  • If you want systems that work reliably, you fix the root cause of a problem. You don't hack around something broken to paper over problems and hope you covered all failure modes - because you won't cover all failure modes. There are clear standards for /bin/sh and accepting software that assumes /bin/sh is the same as /bin/bash is how you get systems that continually fail and are unmaintainable. Your acceptance of such incompetence directly contributes to "a world which is not ideal". – Andrew Henle Sep 12 '18 at 11:11
  • @AlexChe : Honestly, if a maintainer doesn't want to fix bugs in his program, there is nothing we can (nor should) do. Maybe the next maintainer wants to have run his scripts magically by Zsh? – user1934428 Sep 13 '18 at 07:19
  • 1
    Duplicate at https://askubuntu.com/q/1074295 Both have good answers. – sondra.kinsey Mar 08 '19 at 09:37
  • @sondra.kinsey, wow, that question was posted just one minute after mine! Such a coincidence. – Alex Che Mar 08 '19 at 14:13

5 Answers5

3

You can easily fix them, don't break your system!

find . -name '*.sh' -type f -exec sed -i '1s|^#! */bin/sh|#!/bin/bash|' {} +
Ipor Sircer
  • 14,546
  • 1
  • 27
  • 39
  • Maybe not such a good idea to do this in general. It could well be that there is a program somewhere which is supposed to run under sh. Also, there might be shell scripts which do not end in .sh. – user1934428 Sep 12 '18 at 07:45
  • 1
    A restriction to the first line may be appropriate. Furthermore we don’t want to replace interpreters that happen to have /bin/sh as their true path prefix (e. g. things like /bin/sh.static). I would also be a little lenient with white-space between the shebang and the interpreter path (which I saw in the wild and which is permitted by some systems). Summary: sed -i -re '1s,^#!\s*/bin/sh(\s|$),#!/bin/bash\1,' – David Foerster Sep 12 '18 at 07:53
3

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/*
ilkkachu
  • 138,973
  • 1
    It may be a good idea to not replace interpreters whose path have the true prefix /bin/sh (e. g. things like /bin/sh.static): sed -i -re '1s,^#! */bin/sh(\s|$),#!/bin/bash\1,' – David Foerster Sep 12 '18 at 07:56
  • Thanks for the idea about mount namespaces. With the help of it I've managed to achieve what I originally wanted. See my answer. – Alex Che Sep 12 '18 at 10:24
3

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.

  • Thanks! It worked like a charm. I only needed to add -fPIC and move -ldl to the end of the compilation line. – Alex Che Sep 12 '18 at 13:08
1

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:

user@ubuntu:~$ /bin/sh --version
GNU bash, version ...
user@ubuntu:~$ diff -s /bin/sh /bin/bash
Files /bin/sh and /bin/bash are identical

But if running in another shell:

user@ubuntu:~$ /bin/sh --version
/bin/sh: 0: Illegal option --
user@ubuntu:~$ diff -s /bin/sh /bin/bash
Binary files /bin/sh and /bin/bash differ
user@ubuntu:~$ diff -s /bin/sh /bin/dash
Files /bin/sh and /bin/dash are identical
Alex Che
  • 261
  • All that to accommodate blatantly wrong software? Because something like But if running in another terminal: ... is just going to create confusion. – Andrew Henle Sep 12 '18 at 10:57
  • @AndrewHenle, could you please explain what exactly creates confusion? – Alex Che Sep 12 '18 at 11:19
  • "But if running in another terminal" - that means "sometimes it works, sometimes it doesn't". That's inherently confusing and it's not something I'd want to put my name on because the best possible reaction is going to be "Why can't it work all the time?" – Andrew Henle Sep 12 '18 at 11:30
  • I think you probably didn't get it. The described procedure creates an isolated environment with sh pointing to bash, while not touching the original system. This is what 'another terminal' part is about. – Alex Che Sep 12 '18 at 11:35
  • I get it just fine. Do you? What if someone forgets to run "another terminal"? You get the "Why can't this JUNK work?!?!" response. What fails then? You're doing extra work to try to hide problems caused by someone who doesn't know what they're doing. And what happens when you can't run "sudo apt install unionfs-fuse"? How do you plan on papering over the problem then? Have you ever bothered to look at how much more resources are needed to fix problems the later in the development/deployment cycle they're fixed in? You've spent all this effort to work around a one-line fix. – Andrew Henle Sep 12 '18 at 13:02
  • Men, I think you are making it more complex then it is. I've just found a solution which fits my requirements. I've posted it here 'as is' just in case someone finds it useful. I'm not going to distribute and support this solution somehow. It's just for me and for someone who'll use it for his own risk. But I don't mind to give answers if someone asks why my junk isn't working, if I have time. – Alex Che Sep 12 '18 at 13:22
  • Anyway, if you don't like this solution, that's fine. Just give it -1. If someone likes it, he'll give +1. This is how this site's supposed to work. – Alex Che Sep 12 '18 at 13:26
  • 1
    If you want to use namespaces, then please notice that you don't need unionfs; you can simply mount --bind /bin/bash /bin/dash inside the namespace, because mount --bind works with regular files, too. –  Sep 14 '18 at 09:56
  • @mosvy, thanks, it really simplifies the solution! – Alex Che Sep 18 '18 at 08:38
0

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.

schily
  • 19,173
  • I assume that /bin/sh is linked to dash instead of bash for a reason (i.e. performance). So at least I would want to change the link back after running the scripts. Also, this change would be visible to other processes, and possibly break them, and I would like to avoid this as well. Anyway, thanks. I understand, that the problem may not have an ideal solution. – Alex Che Sep 11 '18 at 14:42
  • See this question for background information on dash v. bash in Ubuntu. – Stephen Kitt Sep 11 '18 at 15:11