Home > Enterprise >  Recursion in bash: code after recursion not executed
Recursion in bash: code after recursion not executed

Time:01-20

I have a simple problem I want to solve with a bash script: copy a file, and also copy all the files that are imported in that file, and imported in that file, and so on. This screams recursion.

The files look like this:

import "/path/to/otherfile.txt"
import "/path/to/anotherfile.txt"

information
otherinformation
...

Shouldn't be so hard, here's what I wrote:

#!/bin/bash

destination=/tmp

copy_imports () {
  insfile=$1
  contained_imports=$(grep "import" $insfile | awk -F' ' '{ print $2 }' | sed 's/"//g')
  for imported_insfile in $contained_imports
  do
    copy_imports $imported_insfile
  done

  cp $insfile $destination
}


copy_imports $1

But for some reason, not all files are copied. I see that it is calling the recursion for all the files and nested imports, but not for all of them the cp statement is executed.

I'm totally puzzled, what's going on here?

Thanks a lot!

CodePudding user response:

It looks like your script is trying to recursively copy all the imported files and their imports as well. However, there seems to be an issue with the cp statement not being executed for all the imported files.

One possible reason for this could be that some of the imported files might not be present in the current directory, or they might not have the correct permissions to be copied. To check if the imported files are present in the current directory, you can use the ls command to list all the files in the current directory and see if the imported files are present.

Another issue could be that the contained_imports variable might be empty, which would cause the for loop to not execute, and the cp statement would not be executed for the imported file. You can add a check to see if the variable is empty before the for loop to make sure that the cp statement is always executed.

#!/bin/bash

destination=/tmp

copy_imports () {
  insfile=$1
  contained_imports=$(grep "import" $insfile | awk -F' ' '{ print $2 }' | sed 's/"//g')
  if [ -z "$contained_imports" ]; then
    cp $insfile $destination
    return
  fi
  for imported_insfile in $contained_imports
  do
    copy_imports $imported_insfile
  done

  cp $insfile $destination
}


copy_imports $1

It is also a good practice to add a check if the file exists before trying to copy it using the -f or -e option of the [ command or test command.

if [ -e "$insfile" ]; then
  cp $insfile $destination
fi

It's also worth noting that if the imported files have relative path, the script will not work as expected as the current working directory might change during the recursion. In this case, it might be a good idea to use absolute path for the imported files.

CodePudding user response:

The primary problem is that all of the function's variables need to be made local; without that, all invocations of the function are sharing the same global variables, leading to confusion.

Also, I'd recommend checking whether each imported file has already been copied, to avoid duplicating work. I moved the cp command before the recursive loop, which effectively flags the file as "done" as soon as processing begins on it (rather than waiting until it's been fully processed).

As tripleee pointed out, double-quoting variable references is almost always a good idea ($contained_imports is an exception here, since you're counting on word-splitting to break it into separate filenames; if there was a possibility of filenames with spaces, you'd need to use a more robust method). Finally, I couldn't resist replacing the grep | awk | sed pattern with pure awk.

I haven't tested this, but I think it'll work:

#!/bin/bash

destination=/tmp

copy_imports () {
  local insfile="$1"
  if [[ ! -e "$destination/$(basename "$insfile")" ]]; then
    cp "$insfile" "$destination"
    local contained_imports="$(awk -F' ' '/import/ { gsub("\"", "", $2); print $2 }' "$insfile")"
    local imported_insfile
    for imported_insfile in $contained_imports
    do
      copy_imports "$imported_insfile"
    done
  fi
}


copy_imports "$1"

CodePudding user response:

An other approach would be to get the full list of imported files with awk and pass it to xargs for the actual copying:

destdir=/tmp

mkdir -p "$destdir" || exit 1

awk '
    BEGIN {
        for (i = 1; i < ARGC; i  )
            if ( !(ARGV[i] in imports) ) {
                imports[ARGV[i]]
                while ( (getline line < ARGV[i]) > 0 )
                    if ( line ~ /^import / && match(line, /".*"/) )
                        ARGV[ARGC  ] = substr(line, RSTART 1, RLENGTH-2)
            }
        for (file in imports)
            printf "%s%c", file, 0
    }
' "$@" |

xargs -0 sh -c '[ "$#" -gt 0 ] && cp "$@" "$0"/' "$destdir"

note: This script copies all the files, including the ones provided in argument.

  • Related