简体   繁体   English

Bash脚本:使用通配符删除文件和目录列表的更好方法

[英]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". 即,如果我删除/root/*.tst,请说“我删除了* .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. 似乎希望将i设置为三种模式中的每一种,这有点棘手,应该避免使用,因为您使用的大多数运算符都期望单个文件或目录名称,而不是匹配多个名称的模式。


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 . 将扩展/root/*.tst并尝试在扩展名中将第一个名称用作命令名,该命令在变量file具有文字值/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 . 文字*存储在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. $file扩展为其文字值之后,存储的模式将扩展为匹配文件名的集合。 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 . 名为foo bar的单个文件将被视为分配给i两个单独的值,即foobar The correct way to deal with such file names in bash is to use an array: bash处理此类文件名的正确方法是使用数组:

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. 如果您希望能够在错误消息中打印原义glob,则需要用引号引起来。

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. 引用deleteGlobs的参数。 The $glob variable inside the function is not quoted ( for path in $glob ) which triggers shell expansion at that point. 函数内的$glob变量未加引号( for path in $glob ),这会在此时触发外壳扩展。

Many thanks to everyones' posts including John Kugelman. 非常感谢所有人的帖子,包括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
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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