Home > OS >  my bash code doesn't meet the loop condition, but loops anyway ( does not work)
my bash code doesn't meet the loop condition, but loops anyway ( does not work)

Time:12-14

So, i create a bash script that search recursively for files that includes the words that you introduce, then creates a folder (if the folder does not exist) with the same name as the variable introduced and finally it copies all the files that meet the word in that new folder.

So for example, if i have a folder named "files_1906" and inside i have a file named "hello_1906.pdf" and then another folder (not inside the first) named "hello" and inside a file named "1906hello" and if i introduce: .sh program_name.sh "1906" it should create a folder named 1906 and copy the two files inside.

but right now i only managed to create the folders, it doesn't copy the files.

here is the full code:

#!/bin/bash
currentDir=$(pwd)
for var in "$@"
do
    countCurrentWord=$(find . -type f -name "$var" | grep "$var" | wc -l)
    echo -e "$countCurrentWord"
    echo -e "Number of files matching the keyword $var: $countCurrentWord"
    if [[ $countCurrentWord -gt 0 ]]
    then
        if [[ -d $currentDir/$var ]]
        then
            echo -e "the folder $var already exists, copying the files...\n"
        else
            mkdir "$currentDir"/"$var"
            echo -e "the folder $var does not exist, creating the folder and copying the files...\n"
        fi
        IFS=$'\n'
        array=$(find . -type f -name "$var")
        declare -p array > /dev/null 2>&1
        declare -a array > /dev/null 2>&1
        length=${#array[@]}
        for ((i=0; i < length; i  ))
        do
            cp "${array[i]}" "$currentDir"/"$var"/ > /dev/null 2>&1
            echo -e "hello world"
        done
    else
        echo -e "no files found, no move will be made.\n"
    fi
done

the console returns the following:

first it says that $countCurrentWord = 0, which doesn't make any sense cause there are files that contains "1906"

but here is where this get's trippy:

the program ignores the greater than 0 and starts the loop, creating the folder called 1906, but after, it doesn't copy the file, it remains empty once is created. I get also the hello world from the second loop.

So, how can we fix this so that it does its job and copies the files?

i already tried with files named only "1906" (also does not work), but the script should be capable of reading the full name of a file and discern if contains the keyword introduced. i don't know if i have a typo or anything, i'm not experienced in scripting in bash

I hope i explained this well enough so you guys can help me, thank you for your effort!

CodePudding user response:

Full rewrite: Running find only once for each submited words and use reliable bash variable for storing found file names.

#!/bin/bash
shopt -s extglob
currentDir=$(pwd)
for var ;do
    mapfile  -d '' -t FoundFiles < <(
        find !($var) -type f -iname "*$var*" -print0
    )
    if (( ${#FoundFiles[@]} > 0 )) ;then
        if [[ -d $currentDir/$var ]] ;then
            printf 'Folder %s already exist.\n' "$var"
        else
            printf 'Create new folder '%s'.\n' "$var"
            mkdir "$currentDir/$var"
        fi
        cp -at "$currentDir/$var" "${FoundFiles[@]}"
    else
        printf 'No file found with \47%s\47.\n' "$var"
    fi
done
  • Avoid find to browse folder named $var, by using shopt -s extglob and find !($var).
  • In 4th line, for var; do, the common case in "$@" is implicit.
  • cp -ait "/path/to/target" "${array[@]}" is reliable, work with special file names and so on.

I didn't add echo "hello world" command, but feel free to add, maybe a v flag to cp command:

cp -avt "$currentDir/$var" "${FoundFiles[@]}"

... And/or a i flag to ask before overwrite:

cp -viat "$currentDir/$var" "${FoundFiles[@]}"

CodePudding user response:

1) You're using the $var string as a literal with mkdir, as a filename glob with find and as an extended regex with grep. The characters can have different meanings in each of these contexts.

Given your end-goal, the grep can be dropped, so you just have to make sure that $var keeps a literal meaning when used in a glob. Here's a way to do the glob-escaping with awk:

#!/bin/bash

var='* [string] ?'

IFS='' read -r -d '' var_glob_safe < <(
    LANG=C awk '
        BEGIN {
            gsub(/[\\?*[]/,"\\\\&", ARGV[1])
            printf "%s%c", ARGV[1], 0
        }
    ' "$var"
)

printf '%s\n' "$var_glob_safe"
\* \[string] \?

note: the only valid delimiter for arbitrary paths is the NUL byte.

Well, that's the gist of it; there's no good reason to fire an awk for each $var in $@ because you can do all the glob-escapes in one fell swoop and read them one by one while iterating over $@ :

#!/bin/bash
  
for var in "$@"
do
    IFS='' read -r -d '' var_glob_safe

    # ...

done < <(
    LANG=C awk '
        BEGIN {
            for (i = 1; i < ARGC; i  ) {
                gsub(/[\\?*[]/,"\\\\&", ARGV[i])
                printf "%s%c", ARGV[i], 0
            }
        }
    ' "$@"
)

2) For finding the files that contain $var_glob_safe in their name, the command would be:

find . -type f -name "*$var_glob_safe*"

But you want to fill a bash array with the result, so you have to tell find to use a NUL byte delimiter (with -print0) and populate the array like this:

# for bash >= 4.3
readarray -t -d '' files < <(
    find . -type f -name "*$var_glob_safe*" -print0
)

# for bash < 4.3
files=()
while IFS='' read -r -d '' f
do
    files =( "$f" )
done <(
    find . -type f -name "*$var_glob_safe*" -print0
)

Now you have access to the total number of "files" with ${#files[@]}, so you can base your logic on that.


3) Here's a code that incorporates the above explanations and more; the only missing parts are the echos:

#!/bin/bash
  
for var in "$@"
do
    [[ $var == */* ]] && exit 1 # a filename can't contain a slash

    IFS='' read -r -d '' var_glob_safe

    readarray -d '' files < <(
        find . -type f -name "*$var_glob_safe*" -print0
    )

    if (( ${#files[@]} )) 
    then 
        mkdir -p "$var" || exit 1
        cp "${files[@]}" "$var"
    fi
done < <(
    awk '
        BEGIN {
            for (i = 1; i < ARGC; i  ) {
                gsub(/[\\?*[]/,"\\\\&", ARGV[i])
                printf "%s%c", ARGV[i], 0
            }
        }
    ' "$@"
)
  • Related