-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
  • for DEPLOYMENT_ALIAS in "${ZEIT_DEPLOYMENT_ALIASES[@]}" – schrodingerscatcuriosity Oct 21 '20 at 15:55
  • Thanks (your edit replaced the Attempt 1 though ^-^) – Vadorequest Oct 21 '20 at 15:56
  • Actually, ZEIT_DEPLOYMENT_ALIASES=$(cat now.$CUSTOMER_REF_TO_DEPLOY.staging.json | jq --raw-output '.alias') is what really happens, and since it returns JSON such as ['test.domain'] I assumed it would be similar in bash. (doesn't seem like) – Vadorequest Oct 21 '20 at 16:01
  • As for the then clause, it has been removed by @schrodigerscatcuriosity edit, I edited again. – Vadorequest Oct 21 '20 at 16:02
  • What is test.sh? Are you expecting ZEIT_DEPLOYMENT_ALIASES=['test.sh'] to run the script test.sh and save the script's output in the variable ZEIT_DEPLOYMENT_ALIASES? – terdon Oct 21 '20 at 16:05
  • Bad example, test.sh is meant to be a domain, not an executable, could have named it test.xyz for all that matters – Vadorequest Oct 21 '20 at 16:06
  • 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 Oct 21 '20 at 16:22
  • @Vadorequest, But, looking at what you wrote in the comments, you have ZEIT_DEPLOYMENT_ALIASES=$(cat now.$CUSTOMER_REF_TO_DEPLOY.staging.json | jq --raw-output '.alias'), which is not an array, but a simple scalar string variable. And actually tells us that by null, you meant the actual string null. (I'm not sure if jq can return an empty string there.) But that also tells us that your original data source is a JSON file, and you can use jq... – ilkkachu Oct 21 '20 at 16:24
  • Yeah, sorry for the confusion, definitely a beginner in bash here. I wanted to avoid overcomplexify things and focus on what I thought was the issue at hand. – Vadorequest Oct 21 '20 at 16:27
  • @Vadorequest, also, I'm not exactly sure where you're getting ['test.sh'] from, since AFAIK single quotes are not a valid way to quote strings in JSON, and jq produces strings in double quotes. – ilkkachu Oct 21 '20 at 16:27
  • As for ['test.sh'] I thought it was a valid representation, didn't think single/double quotes had different meanings. – Vadorequest Oct 21 '20 at 16:28
  • 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 Oct 21 '20 at 16:31
  • Exactly. This file has been read by jq. – Vadorequest Oct 21 '20 at 16:33
  • @Vadorequest, thank you. see answer. – ilkkachu Oct 21 '20 at 16:52
  • 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). –  Oct 21 '20 at 18:42

2 Answers2

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 string null, 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...

ilkkachu
  • 138,973
  • 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". – Vadorequest Oct 21 '20 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 Oct 21 '20 at 16:59
  • It didn't change it because it's an array and not a string, but against a string it'd have worked. – Vadorequest Oct 21 '20 at 17:00
  • 2
    @Vadorequest, yep. – ilkkachu Oct 21 '20 at 17:02
  • I get /home/runner/work/_temp/4d02416d-053b-48be-be58-a70c328e0519.sh: line 21: tfp-gem-v4-preview.vercel.app: command not found when running readarray -t entries < <($(cat now.$CUSTOMER_REF_TO_DEPLOY.staging.json | jq --raw-output '.alias[]')), it somehow tries to run a command. (where tfp-gem-v4-preview.vercel.app is what's inside my alias array of 1 element) – Vadorequest Oct 21 '20 at 17:34
  • @Vadorequest, take the inner command substitution out, just < <(cat | jq). Now you have $(cat | jq), which expands the output to the command line, then <( ) tries to run what it sees there... (also, the cat is not necessary, you can just redirect the input file to jq) – ilkkachu Oct 21 '20 at 17:37
  • Still trying but getting nowhere. readarray -t entries < <(jq '.alias[]' < now.$CUSTOMER_REF_TO_DEPLOY.staging.json) errors with syntax error near unexpected token '<' – Vadorequest Oct 21 '20 at 17:54
  • I also looked at https://unix.stackexchange.com/a/314379/60329 which is similar to your answer. – Vadorequest Oct 21 '20 at 17:59
  • @Vadorequest, copied from that comment exactly, it runs in my system (Bash 4.4). Do check that you're actually running Bash, and not some other shell. /bin/sh might be a more limited shell in some systems, e.g. in Debian and Ubuntu. But the error message doesn't match what the Dash shell those two use would give. – ilkkachu Oct 21 '20 at 17:59
  • I don't know exactly what I'm running under, this script is executed by GitHub Actions, but I'll take a look. – Vadorequest Oct 21 '20 at 18:00
  • It had nothing to do with bash, I had tried another command above that was generating the error and forgot about it. – Vadorequest Oct 21 '20 at 18:04
  • 2
    @Vadorequest, ah, ok. – ilkkachu Oct 21 '20 at 18:08
  • Yeah, dumb mistake. Sorry, I saw you took it into account and added a workaround for that edge case. I hope it'll be useful to someone else ;) – Vadorequest Oct 21 '20 at 18:09
  • 1
    Thank you, I managed to make it work after all. – Vadorequest Oct 21 '20 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 &quot;${ZEIT_DEPLOYMENT_ALIASES[@]}&quot;; do
  echo &quot;npx now alias &quot;$ZEIT_DEPLOYMENT_URL $DEPLOYMENT_ALIAS
  npx now alias $ZEIT_DEPLOYMENT_URL $DEPLOYMENT_ALIAS --token $ZEIT_TOKEN || echo &quot;Aliasing failed for '$DEPLOYMENT_ALIAS', but the build will continue regardless.&quot;
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!

  • 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. –  Oct 21 '20 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) – Vadorequest Oct 21 '20 at 19:07