简体   繁体   中英

Bash script: A better way to remove a list of files and directories with wildcards

I am trying to pass a list of files including wildcard files and directories that I want to delete but check for them to see if they exist first before deleted. If they are deleted, notify that the directory was deleted, not each individual file within the directory. Ie if I remove /root/*.tst just say, "I removed *.tst".

#!/bin/bash

touch /boot/{1,2,3,4,5,6,7,8,9,10}.tst
touch /root/{1,2,3,4,5,6,7,8,9,10}.tst
mkdir /root/tmpdir

#setup files and directories we want to delete
file=/boot/*.tst /root/*.tst /root/tmpdir
for i in $file; do
    if [[ -d "$file" || -f "$file" ]]; then #do I exist
        rm -fr $i
        echo removed $i #should tell me that I removed /boot/*.tst or /root/*.tst or /root/tmpdir
    else
        echo $i does not exist # should tell me if /boot/*.tst or /root/*.tst or /root/tmpdir DNE
    fi
done

I can't seem to make any combination of single or double quotes or escaping * make the above do what I want it to do.

Before explaining why your code fails, here is what you should use:

for i in /boot/*.txt /root/*.txt /root/tmpdir; do
    # i will be a single file if a pattern expanded, or the literal
    # pattern if it does not. You can check using this line:
    [[ -f $i ]] || continue
    # or use shopt -s nullglob before the loop to cause non-matching
    # patterns to be silently ignored
    rm -fr "$i"
    echo removed $i
done

It appears that would want i to be set to each of three patterns, which is a little tricky and should probably be avoided, since most of the operators you are using expect single file or directory names, not patterns that match multiple names.


The attempt you show

file=/boot/*.tst /root/*.tst /root/tmpdir

would expand /root/*.tst and try to use the first name in the expansion as a command name, executed in an environment where the variable file had the literal value /boot/*.tst . To include all the patterns in the string, you would need to escape the spaces between them, with either

file=/boot/*.tst\ /root/*.tst\ /root/tmpdir

or more naturally

file="/boot/*.tst /root/*.tst /root/tmpdir"

Either way, the patterns are not yet expanded; the literal * is stored in the value of file . You would then expand it using

for i in $file    # no quotes!

and after $file expands to its literal value, the stored patterns would be expanded into the set of matching file names. However, this loop would only work for file names that didn't contain whitespace; a single file named foo bar would be seen as two separate values to assign to i , namely foo and bar . The correct way to deal with such file names in bash is to use an array:

files=( /boot/*.tst /root/*.tst /root/tmpdir )

# Quote are necessary this time to protect space-containing filenames
# Unlike regular parameter assignment, the patterns were expanded to the matching
# set of file names first, then the resulting list of files was assigned to the array,
# one file name per element.
for i in "${files[@]}"

You can replace

file=/boot/*.tst /root/*.tst /root/tmpdir

by

printf -v file "%s " /boot/*.tst /root/*.tst /root/tmpdir

The shell expands globs automatically. If you want to be able to print the literal globs in an error message then you'll need to quote them.

rmglob() {
    local glob

    for glob in "$@"; do
        local matched=false

        for path in $glob; do
            [[ -e $path ]] && rm -rf "$path" && matched=true
        done

        $matched && echo "removed $glob" || echo "$glob does not exist" >&2
    done
}

rmglob '/boot/*.tst' '/root/*.tst' '/root/tmpdir'

Notice the careful use of quoting. The arguments to deleteGlobs are quoted. The $glob variable inside the function is not quoted ( for path in $glob ) which triggers shell expansion at that point.

Many thanks to everyones' posts including John Kugelman.

This is the code I finally went with that provided two types of deleting. The first is a bit more forceful deleting everything. The second preserved directory structures just removing the files. As per above, note that whitespace in file names is not handled by this method.

rmfunc() {
local glob

for glob in "$@"; do
    local matched=false 
    local checked=true

    for path in $glob; do
        $checked && echo -e "\nAttempting to clean $glob" && checked=false
        [[ -e $path ]] && rm -fr "$path" && matched=true
    done

    $matched && echo -e "\n\e[1;33m[\e[0m\e[1;32mPASS\e[1;33m]\e[0m Cleaned $glob" || echo -e "\n\e[1;33m[\e[0m\e[1;31mERROR\e[1;33m]\e[0m Can't find $glob (non fatal)."   
done
}

# Type 2 removal
xargfunc() {
local glob

for glob in "$@"; do
    local matched=false
    local checked=true 

    for path in $glob; do
        $checked && echo -e "\nAttempting to clean $glob" && checked=false
        [[ -n $(find $path -type f) ]] && find $path -type f | xargs rm -f && matched=true
    done

    $matched && echo -e "\n\e[1;33m[\e[0m\e[1;32mPASS\e[1;33m]\e[0m Cleaned $glob" || echo -e "\n\e[1;33m[\e[0m\e[1;31mERROR\e[1;33m]\e[0m Can't find $glob (non fatal)."   
fi
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM