-1

I'm trying to iterate over a variable that might be either null or an array of strings.

ZEIT_DEPLOYMENT_ALIASES=null or ZEIT_DEPLOYMENT_ALIASES=['domain.sh]

I'm a beginner in bash, I read bash iterate file list, except when empty but I couldn't figure it out.

I tried two different approaches.

The values of ZEIT_DEPLOYMENT_ALIASES actually comes from jq library, which reads JSON.

ZEIT_DEPLOYMENT_ALIASES=$(cat now.$CUSTOMER_REF_TO_DEPLOY.staging.json | jq --raw-output '.alias') 

Approach 1

 ZEIT_DEPLOYMENT_ALIASES=['test.sh'] # Check if there are no aliases configured if [ -z "$ZEIT_DEPLOYMENT_ALIASES" ] then ZEIT_DEPLOYMENT_ALIASES_COUNT=${#ZEIT_DEPLOYMENT_ALIASES[@]} echo "$ZEIT_DEPLOYMENT_ALIASES_COUNT alias(es) found. Aliasing them now..." # For each alias configured, then alias it to the deployed domain for DEPLOYMENT_ALIAS in "${ZEIT_DEPLOYMENT_ALIASES_COUNT[@]}" do echo "npx now alias "$ZEIT_DEPLOYMENT_URL $DEPLOYMENT_ALIAS npx now alias $ZEIT_DEPLOYMENT_URL $DEPLOYMENT_ALIAS --token $ZEIT_TOKEN || echo "Aliasing failed for '$DEPLOYMENT_ALIAS', but the build will continue regardless." done else # $ZEIT_DEPLOYMENT_ALIASES is null, this happens when it was not defined in the now.json file echo "There are no more aliases to configure. You can add more aliases from your now.json 'alias' property. See https://vercel.com/docs/configuration?query=alias%20domain#project/alias" echo "$ZEIT_DEPLOYMENT_ALIASES" fi 

But with this, even when ZEIT_DEPLOYMENT_ALIASES=['something'] it doesn't go into the then clause.

Approach 2

ZEIT_DEPLOYMENT_ALIASES=['test.sh'] echo "Alias(es) for current project:" $ZEIT_DEPLOYMENT_ALIASES for DEPLOYMENT_ALIAS in $ZEIT_DEPLOYMENT_ALIASES; do [ -z "$DEPLOYMENT_ALIAS" ] || continue echo "npx now alias "$ZEIT_DEPLOYMENT_URL $DEPLOYMENT_ALIAS npx now alias $ZEIT_DEPLOYMENT_URL $DEPLOYMENT_ALIAS --token $ZEIT_TOKEN || echo "Aliasing failed for '$DEPLOYMENT_ALIAS', but the build will continue regardless." done 

Similarly, it seems like [ -z "$DEPLOYMENT_ALIAS" ] always evaluate to true.

Here is a playground if you'd like:

  1. https://www.jdoodle.com/iembed/v0/3bs
  2. https://www.jdoodle.com/iembed/v0/3bo
15
  • for DEPLOYMENT_ALIAS in "${ZEIT_DEPLOYMENT_ALIASES[@]}"CommentedOct 21, 2020 at 15:55
  • Thanks (your edit replaced the Attempt 1 though ^-^)CommentedOct 21, 2020 at 15:56
  • 1
    @Vadorequest, not to put too fine a point on it, you're confusing the issue by not showing the actual situation. For example, when you say "a variable that might be either null or an array of strings", people are going to assume you mean an actual array variable in the language in question, a Bash array. And in that context, null is a bit unclear, even though the POSIX text uses it to mean an empty string.
    – ilkkachu
    CommentedOct 21, 2020 at 16:22
  • 2
    @Vadorequest, single quotes are fine for the shell here, no difference to double quotes. But the point is that you were not showing the actual situation, which makes it harder for people to help you. Now. Getting past that, I'm I right to assume that you have a file that contains something like this: {"alias": ["foo", "bar"], "whatever": "xyzzy"}, and you want to process the foo and bar from list in the alias field?
    – ilkkachu
    CommentedOct 21, 2020 at 16:31
  • 1
    @Vadorequest jq knows about the shell's syntax, look at the "Format strings and escaping" part of its manpage. Assuming that you have a json file of the form {"x":["a\nb","c d","e\n f"]}, I think that eval "set -- $(jq -r '.x//empty|@sh' <file)" (followed by e.g. for f; do printf '{%s}' "$f"; done) should do what you want. (You may also omit the //empty part or use the -e option).
    – user313992
    CommentedOct 21, 2020 at 18:42

2 Answers 2

3

Given this file:

$ cat test.json {"alias": ["foo", "bar"], "whatever": "xyzzy"} 

at least my version of jq gives this output for jq --raw-output '.alias[]' < test.json

$ jq --raw-output '.alias[]' < test.json foo bar 

i.e. the entries are on separate lines, which is important, since we can use that to separate them from each other. For example, by reading them to an array with readarray. (<(...) is a process substitution, it makes the output of the command available like it was a file, so < <(...) makes it available in stdin. A bit like a pipe, actually, except that pipes run subshells so the read values would not be available after the pipe.)

#!/bin/bash readarray -t entries < <(jq '.alias[]' < test.json) if [ "${#entries[@]}" = 0 ]; then echo empty array... fi # this will not do anything if the array is empty for entry in "${entries[@]}"; do echo "processing entry $entry..." done 

To deal with a possibly missing alias field, use .alias[]? in jq instead. Note however, that will deal a non-array string value (like {"alias": "foo"}) as empty, so if that's a possibility, we'd need to do something else.

Also, note that if the entries contain newlines, --raw-output will print them as-is, so entries with them will show up split to multiple lines, as if they were multiple distinct entries.


Alternatively, without process substitution, so this should work with a standard shell, not only with Bash.

#!/bin/sh jq --raw-output '.alias[]' < json.txt | ( any= while IFS= read -r line; do echo "doing something with '$line'..." any=1 done if [ "$any" != 1 ]; then echo "empty input..." fi ) 

See Why is my variable local in one 'while read' loop, but not in another seemingly similar loop? as to why the parenthesis are necessary.


Now, as to your code:

ZEIT_DEPLOYMENT_ALIASES=['test.sh'] 

This would assign the string [test.sh] to the variable. That's not the same as assigning the string ["test.sh"] like you'd get from jq, since here, the shell processes the quotes you gave it. They're not processed similarly from the output of a command substitution. That's also a single scalar variable, not an array.

if [ -z "$ZEIT_DEPLOYMENT_ALIASES" ] 

This tests if the string is the empty string, which probably isn't what you meant. Anyway, jq's .alias could give the stringnull, which is not the same as an empty string.

${#ZEIT_DEPLOYMENT_ALIASES[@]} 

This will always be 1, since it's not an array. And for the same reason, the for loop doesn't do what you wanted.

Note that Bash doesn't process JSON itself, if it gets a string like ["foo", "bar"] from a command substitution, it's just that, a string. You need to split it individual entries yourself...

14
  • Thanks for the patience and explanation, I understand much better now. As for your question regarding raw output: jq --raw-output '.alias[0]' will return test.domain while jq '.alias[0]' would return "test.domain".CommentedOct 21, 2020 at 16:56
  • @Vadorequest, ah, yes, you're right. With jq --raw-output '.alias' it didn't change the output and it seems I didn't test again with .alias[]
    – ilkkachu
    CommentedOct 21, 2020 at 16:59
  • 2
    @Vadorequest, yep.
    – ilkkachu
    CommentedOct 21, 2020 at 17:02
  • 2
    @Vadorequest, ah, ok.
    – ilkkachu
    CommentedOct 21, 2020 at 18:08
  • 1
    Thank you, I managed to make it work after all.CommentedOct 21, 2020 at 18:25
0

Here is the solution for Attempt #1

It was tricky because of the null value, which is not empty unlike I first thought.

And the hardest thing was to convert the jq JSON array into a bash array using:

readarray -t ZEIT_DEPLOYMENT_ALIASES < <(jq --raw-output '.alias[]' < now.$CUSTOMER_REF_TO_DEPLOY.staging.json) 

Big thanks to https://unix.stackexchange.com/a/615717/60329

 ZEIT_DEPLOYMENT_ALIASES_JSON=$(cat now.$CUSTOMER_REF_TO_DEPLOY.staging.json | jq --raw-output '.alias') echo "Custom aliases: " $ZEIT_DEPLOYMENT_ALIASES_JSON # Convert the JSON array into a bash array - See https://unix.stackexchange.com/a/615717/60329 readarray -t ZEIT_DEPLOYMENT_ALIASES < <(jq --raw-output '.alias[]' < now.$CUSTOMER_REF_TO_DEPLOY.staging.json) # Check if there are no aliases configured (it will return "null" in such case, which is not the same as bash "empty") if [ "$ZEIT_DEPLOYMENT_ALIASES" != null ] then ZEIT_DEPLOYMENT_ALIASES_COUNT=${#ZEIT_DEPLOYMENT_ALIASES[@]} echo "$ZEIT_DEPLOYMENT_ALIASES_COUNT alias(es) found. Aliasing them now..." # For each alias configured, then assign it to the deployed domain for DEPLOYMENT_ALIAS in "${ZEIT_DEPLOYMENT_ALIASES[@]}"; do echo "npx now alias "$ZEIT_DEPLOYMENT_URL $DEPLOYMENT_ALIAS npx now alias $ZEIT_DEPLOYMENT_URL $DEPLOYMENT_ALIAS --token $ZEIT_TOKEN || echo "Aliasing failed for '$DEPLOYMENT_ALIAS', but the build will continue regardless." done else # $ZEIT_DEPLOYMENT_ALIASES is null, this happens when it was not defined in the now.json file echo "There are no more aliases to configure. You can add more aliases from your now.json 'alias' property. See https://vercel.com/docs/configuration?query=alias%20domain#project/alias" echo "$ZEIT_DEPLOYMENT_ALIASES" fi 

Thank you!

2
  • 1
    As I already mentioned in a comment, have a look at jq's // and empty ops and its @sh format, instead of trying to do it in the shell. Besides being more complicated, your solution has problems with strings which contain newlines.
    – user313992
    CommentedOct 21, 2020 at 18:47
  • Thanks for pointing those issues out. I'm not likely to change the script because the only expected strings are domain names, and they can't contain weird chars such as newlines. The whole thing is quite complicated/long to test and I'm really not looking forward spending another hour on this. (to be honest)CommentedOct 21, 2020 at 19:07

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.