How to escape space in bash script from inline if? - bash

I know that similar questions have been asked and answered before on stackoverflow (for example here and here) but so far I haven't been able to figure it out for my particular case.
I'm trying to create a script that adds the -v flag only if the variable something is equal to "true" (what I'm trying to do is to mount the current folder as a volume located at /src in the Docker container):
docker run --name image-name `if [ "${something}" == "true" ]; then echo "-v $PWD:/src"; fi` ....
The problem is that $PWD may contain spaces and if so my script won't work. I've also tried assigning "$PWD" to an intermediate variable but it still doesn't work:
temp="$PWD"
docker run --name image-name `if [ "${something}" == "true" ]; then echo "-v $temp:/src"; fi` ....
If I run:
docker run --name image-name -v "$PWD":/src ....
from plain bash (without using my script) then everything works.
Does anyone know how to solve this?

Use an array.
docker_args=()
if something; then
docker_args+=( -v "$PWD/src" )
fi
docker run --blah "${docker_args[#]}" …
Don't have arrays? Use set (in a function, so it doesn't affect outer scope).
Generally:
knacker() {
if something; then
set -- -v "$PWD:/src" "$#"
fi
crocker "$#"
}
knacker run --blah
But some commands (like docker, git, etc) need special treatment because of their two-part command structure.
slacker() {
local cmd="$1"
shift
if something; then
set -- -v "$PWD:/src" "$#"
fi
docker "$cmd" "$#"
}
slacker run --blah

Try this (using the array way):
declare -a cmd=()
cmd+=(docker run --name image-name)
if [ "${something}" = "true" ]
then
cmd+=(-v "$PWD:/src")
fi
"${cmd[#]}"

Related

Environment Variables inside the container, but not used in the bash script

I have an application that runs inside a docker container. First I build the image and then run the container. My run command is:
docker run --rm -it -e MODE=custom -e Station=RT -e StartDateReport=2022-09-10 -e Period=1 my-image:1.0.0
I declare the variables MODE, Station, StartDateReport and Period as environment variables. When I start a terminal from the container and type echo $MODE I will get the correct value, custom.
So far, so good, but I am interested in using these variables in a bash script. For example in start.sh I have the following code:
#!/bin/bash
if [[ $MODE == custom ]]; then
// do sth
fi
and here inside the script my variable MODE is undefined, and hence I obtain wrong results.
EDIT
As discussed in the comments below, my application if based on a cronjob to start running.
I managed to solve by myself the problem and the answer is in the comments.
In your environment, does your variable definition have the form
export MODE="custom"
Modified version of your script:
#!/bin/bash
test -z "${MODE}" && ( echo -e "\n\t MODE was not exported from calling environment.\n" ; exit 1 )
if [[ $MODE == custom ]]
then
#// do sth
echo "do sth"
fi
I found the solution for this problem, so I will post the answer here to help others that have the same problem. I found the solution here: How to load Docker environment variables in container
I included export xargs --null --max-args=1 echo < /proc/1/environ in start.sh
Thus, start.sh will be:
#!/bin/bash
export xargs --null --max-args=1 echo < /proc/1/environ
if [[ $MODE == custom ]]; then
// do sth
fi

Use Dockerfile commands inside of a shell script?

Suppose that I have an entry-point to a shell script as I want to use some conditionals in a dockerfile. Is there a way to do something like this?
ENTRYPOINT ["./entry.sh", "lambda-name"]
Inside of entry.sh
#!/usr/bin/env bash
lambda_name=$1
echo "$lambda_name"
if [ "$lambda_name" = "a.handler" ]; then
CMD [ "a.handler" ]
elif [ "$lambda_name" = "b.handler" ];then
CMD [ "b.handler" ]
else
echo "not found"
fi
first of all you don't need that complication.
why not like this?
#!/usr/bin/env bash
lambda_name=$1
echo "$lambda_name"
if [ "$lambda_name" = "a.handler" ]; then
./a.handler
elif [ "$lambda_name" = "b.handler" ];then
./b.handler
else
echo "not found"
fi
also in your script you could use something like
exec "$#"
at the end of your script. this would run all your arguments.
The ENTRYPOINT is the main container process. It executes when you docker run the built image; it's too late to run any other Dockerfile directives at that point.
In particular, the ENTRYPOINT gets passed the image's CMD (or a Compose command: or an alternate command after the docker run image-name) as arguments, and it can do whatever it wants with that. So at this point you don't really need to "set the container's command", you can just execute whatever command it is you might have wanted to run.
#!/bin/sh
case "$1" in
a.handler) a.handler ;;
b.handler) b.handler ;;
*)
echo "$1 not found" >&2
exit 1
;;
esac
With this setup, a fairly common Docker pattern is just to take whatever gets passed as arguments and to run that at the end of the entrypoint script.
#!/bin/sh
# ... do any startup-time setup required here ...
exec "$#"
That matches what you show in the question: if the first argument is a.handler then run that command, and so on.

Bash - Check If given argument exits

I have a separate shell script that runs my docker-compose environment in attached mode or detached if I pass -d or --detach argument. It works fine when I pass this argument (./run-env.sh -d) but it doesn't when I run my script without any option ( ./run-env, just getting blank output and docker-compose doesn't run), where can be a problem?
#!/usr/bin/env bash
for arg in "$#"; do
if [ "$arg" = '-d' ] || [ "$arg" = '--detach' ]
then
docker-compose -f docker-compose.local-environment.yml up --build -V --detach
else
docker-compose -f docker-compose.local-environment.yml up --build -V --abort-on-container-exit
fi
done
When you don't give argument, you don't even enter the for loop, that's why nothing happens.
#!/usr/bin/env bash
# By default, use '--abort-on-container-exit' option
abort_or_detach="--abort-on-container-exit"
# Search for a parameter asking to run in detached mode
for arg in "$#"; do
if [ "$arg" = '-d' ] || [ "$arg" = '--detach' ]
then
abort_or_detach="--detach"
fi
done
# Run with the correct option
docker-compose -f docker-compose.local-environment.yml up --build -V $abort_or_detach
Here in this script, you call one time docker-compose, and you can manage easily the options with the for loop
Also, with your first try, you would launch docker-compose as many times as you have different parameters. Here, you treat them, and then do a single launch
for arg in "$#" iterates over the arguments. When you pass no arguments, it iterates zero times. Instead, try something like:
extra=--abort-on-container-exit
for arg; do
case "$arg" in
-d|--detach) extra=--detach
esac
done
docker-compose -f docker-compose.local-environment.yml up --build -V $extra
Note that this is one of those cases where you do not want to put quotes around $extra, because if extra is the empty string you don't want to pass anything to docker-compose. (Here, the default will ensure it is not empty, but this is a fairly common pattern and there are cases where it will be the empty string.)

Expand environment variable inside container on Docker command line

Suppose that I create a Dockerfile that just runs an echo command:
FROM alpine
ENTRYPOINT [ "echo" ]
and that I build it like this:
docker build -t my_echo .
If I run docker run --rm my_echo test it will output test as expected.
But how can I run the command to display an environment variable that is inside the container?
Example:
docker run --rm --env MYVAR=foo my_echo ???
How to access the $MYVAR variable that is in the container to display foo by replacing the ??? part of that command?
Note:
This is a simplified version of my real use case. My real use case is a WP-CLI Docker image that I built with a Dockerfile. It has the wp-cli command as the ENTRYPOINT.
I am trying to run a container based on this image to update a WordPress parameter with an environment variable. My command without Docker is wp-cli option update siteurl "http://example.com" where http://example.com would be in an environment variable.
This is the command I am trying to run (wp_cli is the name of my container):
docker run --rm --env WEBSITE_URL="http://example.com" wp_cli option update siteurl ???
It's possible to have the argument that immediately follows ["bash", "-c"] itself be a shell script that looks for sigils to replace. For example, consider the following script, which I'm going to call argEnvSubst:
#!/usr/bin/env bash
args=( "$#" ) # collect all arguments into a single array
for idx in "${!args[#]}"; do # iterate over the indices of that array...
arg=${args[$idx]} # ...and collect the associated values.
if [[ $arg =~ ^#ENV[.](.*)#$ ]]; then # if we have a value that matches a pattern...
varname=${BASH_REMATCH[1]} # extract the variable name from that pattern
args[$idx]=${!varname} # and replace the value with a lookup result
fi
done
exec "${args[#]}" # run our resulting array as a command.
Thus, argEnvSubst "echo" "#ENV.foobar#" will replace #ENV.foobar# with the value of the environment named foobar before it invokes echo.
While I would strongly suggest injecting this into your Dockerfile as a separate script and naming that script as your ENTRYPOINT, it's possible to do it in-line:
ENTRYPOINT [ "bash", "-c", "args=(\"$#\"); for idx in \"${!args[#]}\"; do arg=${args[$idx]}; if [[ $arg =~ ^#ENV[.](.*)#$ ]]; then varname=${BASH_REMATCH[1]}; args[$idx]=${!varname}; fi; done; \"${args[#]}\"", "_" ]
...such that you can then invoke:
docker run --rm --env WEBSITE_URL="http://example.com" \
wp_cli option update siteurl '#ENV.WEBSITE_URL#'
Note the use of bash -- this means alpine (providing only dash) isn't sufficient.

In this simple Docker wrapper script example, how may one correctly pass a current working directory path which contains spaces?

My Docker wrapper script works as intended when the current working directory does not contain spaces, however there is a bug when it does.
I have simplified an example to make use of the smallest official Docker image I could find and a well known GNU core utility. Of course this example is not very useful. In my real world use case, a much more complicated environment is packaged.
Docker Wrapper Script:
#!/usr/bin/env bash
##
## Dockerized ls
##
set -eux
# Only allocate tty if one is detected
# See https://stackoverflow.com/questions/911168/how-to-detect-if-my-shell-script-is-running-through-a-pipe
if [[ -t 0 ]]; then
DOCKER_RUN_OPTIONS+="-i "
fi
if [[ -t 1 ]]; then
DOCKER_RUN_OPTIONS+="-t "
fi
WORK_DIR="$(realpath .)"
DOCKER_RUN_OPTIONS+="--rm --user=$(id -u $(logname)):$(id -g $(logname)) --workdir=${WORK_DIR} --mount type=bind,source=${WORK_DIR},target=${WORK_DIR}"
exec docker run ${DOCKER_RUN_OPTIONS} busybox:latest ls "$#"
You can save this somewhere as /tmp/docker_ls for example. Remember to chmod +x /tmp/docker_ls
Now you are able to use this Dockerized ls in any path which contains no spaces as follows:
/tmp/docker_ls -lah
/tmp/docker_ls -lah | grep 'r'
Note that /tmp/docker_ls -lah /path/to/something is not implemented. The wrapper script would have to be adapted to parse parameters and mount the path argument into the container.
Can you see why this would not work when current working directory path contains spaces? What can be done to rectify it?
Solution:
#david-maze's answer solved the problem. Please see: https://stackoverflow.com/a/55763212/1782641
Using his advice I refactored my script as follows:
#!/usr/bin/env bash
##
## Dockerized ls
##
set -eux
# Only allocate tty if one is detected. See - https://stackoverflow.com/questions/911168
if [[ -t 0 ]]; then IT+=(-i); fi
if [[ -t 1 ]]; then IT+=(-t); fi
USER="$(id -u $(logname)):$(id -g $(logname))"
WORKDIR="$(realpath .)"
MOUNT="type=bind,source=${WORKDIR},target=${WORKDIR}"
exec docker run --rm "${IT[#]}" --user "${USER}" --workdir "${WORKDIR}" --mount "${MOUNT}" busybox:latest ls "$#"
If your goal is to run a process on the current host directory as the current host user, you will find it vastly easier and safer to use a host process, and not an isolation layer like Docker that intentionally tries to hide these things from you. For what you’re showing I would just skip Docker and run
#!/bin/sh
ls "$#"
Most software is fairly straightforward to install without Docker, either using a package manager like APT or filesystem-level isolation like Python’s virtual environments and Node’s node_modules directory. If you’re writing this script then Docker is just getting in your way.
In a portable shell script there’s no way to make “a list of words” in a way that keeps their individual wordiness. If you know you’ll always want to pass some troublesome options then this is still fairly straightforward: include them directly in the docker run command and don’t try to create a variable of options.
#!/bin/sh
RM_IT="--rm"
if [[ -t 0 ]]; then RM_IT="$RM_IT -i"; fi
if [[ -t 1 ]]; then RM_IT="$RM_IT -t"; fi
UID=$(id -u $(logname))
GID=$(id -g $(logname))
# We want the --rm -it options to be expanded into separate
# words; we want the volume options to stay as a single word
docker run $RM_IT "-u$UID:$GID" "-w$PWD" "-v$PWD:$PWD" \
busybox \
ls "$#"
Some shells like ksh, bash, and zsh have array types, but these shells may not be present on every system or environment (your busybox image doesn’t have any of these for example). You also might consider picking a higher-level scripting language that can more explicitly pass words into an exec type call.
I'm taking a stab at this to give you something to try:
Change this:
DOCKER_RUN_OPTIONS+="--rm --user=$(id -u $(logname)):$(id -g $(logname)) --workdir=${WORK_DIR} --mount type=bind,source=${WORK_DIR},target=${WORK_DIR}"
To this:
DOCKER_RUN_OPTIONS+="--rm --user=$(id -u $(logname)):$(id -g $(logname)) --workdir=${WORK_DIR} --mount type=bind,source='${WORK_DIR}',target='${WORK_DIR}'"
Essentially, we are putting the ' in there to escape the space when the $DOCKER_RUN_OPTIONS variable is evaluated by bash on the 'exec docker' command.
I haven't tried this - it's just a hunch / first shot.

Resources