5

I've been tracking down an issue I've been facing in a buildkite script, and here's what I've got:

Firstly, I enter the shell of a docker image:

docker run --rm -it --entrypoint bash node:12.21.0

This docker image doesn't have any text editors, so I create my shell scripts by concating to a file:

touch a.sh
chmod +x a.sh
printf '#!/bin/sh\necho ${1:0:1}' >> a.sh

touch b.sh
chmod +x b.sh
printf '#!/bin/bash\necho ${1:0:1}' >> b.sh

I now run my scripts:

./a.sh hello
>./a.sh: 2: ./a.sh: Bad substitution 
./b.sh hello 
>h

Can someone tell me in simple terms what the issue is here?

This AskUbuntu question says that bash and sh are different shells, and that in many systems sh will symlink to bash.

What's going on specifically on this docker image? How would I know?

2
  • 2
    Obviously, in this image, bash != sh. To be certain, docker exec IMAGE ls -l /bin/bash /bin/sh. I suppose that the Bourne Shell doesn't know string substitution. – berndbausch Jun 16 at 9:40
  • Why don't you create your own docker image (possibly deriving the node:12.21.0 one). – xenoid Jun 17 at 10:59
25

/bin/sh is only expected to be a POSIX shell, and the POSIX shell doesn’t know about substrings in parameter expansions.

POSIX “defines a standard operating system interface and environment, including a command interpreter (or “shell”)”, and is the standard largely followed in traditional Unix-style environments. See What exactly is POSIX? for a more extensive description. In POSIX-inspired environments, /bin/sh is supposed to provide a POSIX-style shell, and in a script using /bin/sh as its shebang, you can only rely on POSIX features (even though most actual implementations of /bin/sh provide more). It’s perfectly OK to rely on a more advanced shell, but the shebang needs to be adjusted accordingly.

Since your script relies on a bash feature, the correct shebang is #!/bin/bash (or perhaps #!/usr/bin/env bash), regardless of the environment it ends up running in. It may happen to work in some cases with #!/bin/sh, but that’s just a happy accident.

0
8

Your script relies on a non-POSIX expansion to parameter expansion, so it won't work on most shells.

This can be solved either by forcing the script to be executed by a shell like bash or by writing a POSIX shell script, which is a good habit in many cases.

Unfortunally, there is no elegant way to get the first char in a parameter like in zsh with $1[1]. So use can use

  • "$(echo "$1" | cut -c1-1)" or
  • "$(printf %.1s "$1")" or
  • the weird "${1%"${1#?}"}" which removes the string without first character (${1#?}) from the string
2
  • expr may also be an option – user10489 Jun 16 at 12:11
  • 5
    That last example is rather enlightening. I never knew you could do this ina POSIX shell without invoking a command. Also, it’s arguably a great example of useful shell script being a pain in the arse to read... – Austin Hemmelgarn Jun 16 at 18:00

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.