1
\$\begingroup\$

I've got the following shell script (let say get_includes.sh):

#!/usr/bin/env bash
includes=($(grep ^#include file.c | grep -o '"[^"]\+"' | tr -d '"'))
echo "0: ${includes[0]}"
echo "1: ${includes[1]}"

which aims at finding relative include files in source code file.

So for given file like this (file.c):

#include "foo.h"
#include "bar.h"

#include <stdio.h>

int main(void) {
    printf("Hello World\n");
    return 0;
}

It'll return the following results which are correct:

$ ./get_includes.sh 
0: foo.h
1: bar.h

The code works as expected, however shellcheck complains about the following issues:

$ shellcheck get_includes.sh 

    In get_includes.sh line 2:
    includes=($(grep ^#include file.c | grep -o '"[^"]\+"' | tr -d '"'))
              ^-- SC2207: Prefer mapfile or read -a to split command output (or quote to avoid splitting).

    For more information:
      https://www.shellcheck.net/wiki/SC2207 -- Prefer mapfile or read -a to spli...
      https://www.shellcheck.net/wiki/SC2236 -- Use -n instead of ! -z.

So:

  • I can't quote command substitution, as I expect the command to expand to an array.
  • I don't want to ignore the warning, I'd like to correct it.

I'm using Bash 4.

So, how I can correct the above line to satisfy shellcheck? If possible, I'd like to keep it in one-liner.


I've tried the following approaches which failed:

$ (grep ^#include file.c | grep -o '"[^"]\+"' | read -a myarr; echo $myarr)
(nothing is printed)
$ (grep ^#include file.c | grep -o '"[^"]\+"' | mapfile myarr; echo $myarr)
(nothing is printed)
\$\endgroup\$
2
\$\begingroup\$

... so what's going on?

$ (grep ^#include file.c | grep -o '"[^"]\+"' | read -a myarr; echo $myarr)
(nothing is printed)
$ (grep ^#include file.c | grep -o '"[^"]\+"' | mapfile myarr; echo $myarr)
(nothing is printed)

Why doesn't this work? Because "each command in a pipeline is executed in its own subshell". As the page explains, you could enable the lastpipe option to have the last element of the pipeline run in the current process. (The page doesn't mention that this will only work when you use it in a script, where job control is not active (as mentioned here). It won't work in an interactive shell.)

Keep pipelines as short as possible

It's good to use the fewest possible processes in pipelines. Multiple grep in a pipeline are points to look at with suspicion. You can make the first grep work a little harder, using a stricter pattern, and then you can achieve the same result with 2 processes instead of 3:

grep '^#include .*"' file.c | cut -d'"' -f2
\$\endgroup\$
  • \$\begingroup\$ What would have worked is ... | { read -a myarr; echo "$myarr"; } but it would have made for quite ugly code. \$\endgroup\$ – Kusalananda Mar 25 at 8:10
0
\$\begingroup\$

After reading more about SC2207, the proper syntax would be:

mapfile -t includes < <(grep ^#include file.c | grep -o '"[^"]\+"' | tr -d '"')
\$\endgroup\$

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.