3

When APP_ENV is "prod" then DATABASE_DISABLE_MIGRATIONS should be empty; in any other case, it should be true unless DATABASE_DISABLE_MIGRATIONS is empty or null. In other words, its value must be respected and the automatic assignment should take place only when it's unset.

Original DATABASE_DISABLE_MIGRATIONS APP_ENV New DATABASE_DISABLE_MIGRATIONS
null or empty any retain original value
non-zero length string any retain original value
unset "prod" empty
unset empty or other than "prod" "true"

I'm beginning learning shell (POSIX) and I've managed to accomplish this using the following code:

#!/bin/sh

if [ "${DATABASE_DISABLE_MIGRATIONS+set}" != set ]; then
    : ${DATABASE_DISABLE_MIGRATIONS:=$( [ "$APP_ENV" = "prod" ] || echo "true" )}
fi

# Just for debug (not meant to be in the single-line assignment of course)
if [ -z "$DATABASE_DISABLE_MIGRATIONS" ]; then
    echo "Empty value, migrations ENABLED"
else
    echo "Not empty value, migrations DISABLED"
fi

Is possible to make a single-line statement of the code above?

When considering unset, null or empty string, it's easy to make single-line statement, I often ending up using:

: ${FOO:=$( [ "$APP_ENV" = "prod" ] || echo "true" )}

Variable FOO get the value true when FOO is unset, null or empty and APP_ENV isn't "prod".

Maybe there is a similar "shorcut" that I'm missing?

enter image description here

1

6 Answers 6

5

A case statement using 2 variable might be what you're after:

case "${DATABASE_DISABLE_MIGRATIONS}:${APP}" in
    ?*:* | "":prod) # not empty, or prod
                    echo "Not empty value, migrations DISABLED"
                    ;;
    *) echo "Empty value, migrations ENABLED"
       ;;
esac

Although this does not distinguish between "empty" and "unset". It can also be deceived if one of the variable values contains a colon.


Based on

Original DATABASE_DISABLE_MIGRATIONS APP_ENV New DATABASE_DISABLE_MIGRATIONS
null or empty any retain original value
non-zero length string any retain original value
unset "prod" empty
unset empty or other than "prod" "true"

Without restricting myself to POSIX, and since "unset" is the condition to modify the variable, I'd:

if [[ ! -v DATABASE_DISABLE_MIGRATIONS ]]; then
    if [[ $APP == prod ]]; then 
        DATABASE_DISABLE_MIGRATIONS=""
    else
        DATABASE_DISABLE_MIGRATIONS=true
    fi
fi

That obviously does not address your one-liner wish, but I think it's the most readable way to convey your intent.

2
5

Command:

[ -z "${DATABASE_DISABLE_MIGRATIONS+x}" ] && [ "$APP_ENV" != "prod" ] && DATABASE_DISABLE_MIGRATIONS=true

Explanation:

A comment states that the variable will be checked with
[ -z "$DATABASE_DISABLE_MIGRATIONS" ]
This information should be part of the question.

The result of the expansion "$DATABASE_DISABLE_MIGRATIONS" is the same for an unset variable or an empty string unless you use set -u (or set -o nounset), so the requirements can be simplified. Instead of replacing an unset variable with an explicit empty value in case "prod" you can leave it unset and you will get the same test result.

So the algorithm can be reduced to

  • if ( DATABASE_DISABLE_MIGRATIONS is unset and APP_ENV is not "prod" ) set DATABASE_DISABLE_MIGRATIONS to "true"
  • (otherwise do nothing)

Of course you can write this in multiple lines which would make the code clearer, but it's not a one-liner and not much shorter than your solution in the question.

if [ -z "${DATABASE_DISABLE_MIGRATIONS+x}" ] && [ "$APP_ENV" != "prod" ]
then
    DATABASE_DISABLE_MIGRATIONS=true
fi
1
  • Although clever, the assignment is pushed to the right in a subexpression with a side-effect. In general, programming logic is easier to follow if the side effects (like variable assignment) are easy to locate. On the other hand, the original poster asked for a one-liner so hats off to the solution. Jun 29, 2023 at 7:52
4

So, if the variable is unset, then you want to set it to a default value, and otherwise keep the original value? (With the default value here depending on another variable but let's not allow that to distract.)

Probably best to do just that, then:

# don't disable migrations in production by default
if [ "$APP_ENV" = prod ]; then
    default_DATABASE_DISABLE_MIGRATIONS=
else
    default_DATABASE_DISABLE_MIGRATIONS=true
fi
# set default value if unset
if [ "${DATABASE_DISABLE_MIGRATIONS+set}" != set ]; then
    DATABASE_DISABLE_MIGRATIONS=$default_DATABASE_DISABLE_MIGRATIONS
fi

or a bit shorter:

# don't disable migrations in production by default
default_DATABASE_DISABLE_MIGRATIONS=true
if [ "$APP_ENV" = prod ]; then
    default_DATABASE_DISABLE_MIGRATIONS=
fi
# set default value if unset
: "${DATABASE_DISABLE_MIGRATIONS=$default_DATABASE_DISABLE_MIGRATIONS}"

Code-golfing that based on the values in question appears counterproductive as it then requires you to modify the whole code if you ever need to use similar logic with other values. It's better to make a direct solution that can be applied to other similar cases. Though that last line is pretty much what you ask for in the question title, a single-line shell assignment but only if variable is unset. (With "${var:=default}" it would apply to a set but empty value also.)

I'll also note that the final variable might be clearer to the reader if it was not negated. You mentioned testing the variable with [ -z "$DATABASE_DISABLE_MIGRATIONS" ] and that basically reads like "if not migrations disabled". Which is the same as "if migrations enabled" which would be easier to write if the variable was $DATABASE_ENABLE_MIGRATIONS instead.

0
2

It depends on your definition for a "single line". For instance, you could write your if statement in a single line by using semicolons:

if [ "${DATABASE_DISABLE_MIGRATIONS+set}" != set ]; then ${DATABASE_DISABLE_MIGRATIONS:=$( [ "$APP_ENV" = "prod" ] || echo "true" )}; fi

If your definition of a "single line" is that it should not contain semicolons, you could translate your first if statement to a single line the following way, but it would work exactly the same as your if.

[ "${DATABASE_DISABLE_MIGRATIONS+set}" != set ] && ${DATABASE_DISABLE_MIGRATIONS:=$( [ "$APP_ENV" = "prod" ] || echo "true" )}

Or in a bit shorter version:

[ -z "${DATABASE_DISABLE_MIGRATIONS+set}" ] && ${DATABASE_DISABLE_MIGRATIONS:=$( [ "$APP_ENV" = "prod" ] || echo "true" )}

If by "single line" you mean a "single assignment", you could still use a similar technique:

DATABASE_DISABLE_MIGRATIONS=$([ -z ${DATABASE_DISABLE_MIGRATIONS+set} ] && ( [ "$APP_ENV" = "prod" ] || echo "true" ) || echo ${DATABASE_DISABLE_MIGRATIONS})
2

Could be:

case ${DATABASE_DISABLE_MIGRATIONS++}:$APP_ENV in
  (:prod) DATABASE_DISABLE_MIGRATIONS=    ;;
  (:*   ) DATABASE_DISABLE_MIGRATIONS=true;;
esac

Just like any code, you can put it on one line, here by just concatenating the lines together (and even condensing a bit like

case ${DATABASE_DISABLE_MIGRATIONS++}:$APP_ENV in(:prod)DATABASE_DISABLE_MIGRATIONS=;;(:*)DATABASE_DISABLE_MIGRATIONS=true;esac

) but I don't see the benefit.

Doing $(... echo ...) means (except in ksh93) forking an extra process, running an echo command and send and receive that over a pipe meaning hundreds of thousands if not more of CPU instructions while those simple shell assignments and pattern matching would only be in the hundreds, so it feels a bit silly to me.

1

I don't think it's possible to produce a comprehensible one liner. I would simply use the logic

if [ -z "${DATABASE_DISABLE_MIGRATIONS+dummy}" ]; then
    if [ "${APP_ENV-}" = prod ]; then
        DATABASE_DISABLE_MIGRATIONS=
    else
        DATABASE_DISABLE_MIGRATIONS=true
    fi
fi

where ${x+dummy} is null iff x is unset and ${APP_ENV-} is null if APP_ENV is unset. Here is a complete test:

set -o errexit -o nounset

SetMigrationStatus()
{
    if [ -z "${DATABASE_DISABLE_MIGRATIONS+dummy}" ]; then
        if [ "${APP_ENV-}" = prod ]; then
            DATABASE_DISABLE_MIGRATIONS=
        else
            DATABASE_DISABLE_MIGRATIONS=true
        fi
    fi
}

# Unit tests

unset DATABASE_DISABLE_MIGRATIONS
unset APP_ENV
SetMigrationStatus
if [ "$DATABASE_DISABLE_MIGRATIONS" != true ]; then
    echo Test one failed >&2
    exit 1
fi

unset DATABASE_DISABLE_MIGRATIONS
APP_ENV=
SetMigrationStatus
if [ "$DATABASE_DISABLE_MIGRATIONS" != true ]; then
    echo Test two failed >&2
    exit 1
fi

unset DATABASE_DISABLE_MIGRATIONS
APP_ENV=prod
SetMigrationStatus
if [ -n "$DATABASE_DISABLE_MIGRATIONS" ]; then
    echo Test three failed >&2
    exit 1
fi

DATABASE_DISABLE_MIGRATIONS=foo
unset APP_ENV
SetMigrationStatus
if [ "$DATABASE_DISABLE_MIGRATIONS" != foo ]; then
    echo Test four failed >&2
    exit 1
fi

By the way, to make sure that your script adheres to the POSIX shell standard you can use the shellcheck command with the s option:

 shellcheck -s sh myscript.sh
6
  • 1
    That [ -n "${APP_ENV+dummy}" ] appears unnecessary, since $APP_ENV can only be prod if it's also set, so [ "$APP_ENV" = prod ] already does the same test.
    – ilkkachu
    Jun 28, 2023 at 18:34
  • @ilkkachu With set -o nounset the expansion $APP_ENV will lead to a runtime error if APP_ENV is unset. I find the nounset option useful since it detects errors in the code, like misspelled variables. Jun 29, 2023 at 7:38
  • oh right, good point. Though you could also use [ "${APP_ENV+}" = "prod" ] as it doesn't really matter here if it's unset or set but empty.
    – ilkkachu
    Jun 29, 2023 at 9:06
  • @ilkkachu The expression "${APP_ENV+}" will be always be null so the comparison will always be false. On the other hand "${APP_ENV:-}" = prod will work. I have updated the answer now. Jun 29, 2023 at 15:21
  • argh, I meant "${APP_ENV-}", of course, sorry about that
    – ilkkachu
    Jun 29, 2023 at 16:41

This site is temporarily in read-only mode and not accepting new answers.

Not the answer you're looking for? Browse other questions tagged .