1

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?

  • 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 0: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 7: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 7: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
  • 1
    Duplicate at askubuntu.com/q/1074295 Both have good answers. – sondra.kinsey Mar 8 at 9:37
4

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

find . -name '*.sh' -type f -exec sed -i '1s|^#! */bin/sh|#!/bin/bash|' {} +
  • 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 7: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 7: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/*
  • 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 7: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
  • 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
  • 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. – mosvy Sep 14 '18 at 9:56
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.

  • 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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.