marlemion
marlemion

Reputation: 43

bash: Expand list of arguments and pass it to find - escaping/white spaces hell

I want to check a folder for files and delete some of them. One condition is to keep all files of a certain type (e.g. .txt) and also keep all files with the names of the first search but with different extensions ([names of first search].). All other files in the directory should be deleted.

This can easily be achieved by the find . -type f -not -name xxx command. However, I would like to populate the find command for each [name of first search] found automatically.

To do so I wrote this litte script

#!/bin/bash

while read filename; do
     filename=$(echo $filename | sed 's/\ /\\\ /g')
     filename=\'$filename*\'
     file_list=$file_list" -not -name $filename"
done <<<"$(ls *.txt | sed 's/.txt//g')"

find . -type f $file_list -print0| while read -d $'\0' FILE
     do
     rm -f "$FILE"
done

The $file_list is nicely populated with the respective data, however, find fails saying:

find: unknown predicate `-\'

in case I use the sed command (' ' -> '\ ') or

find: paths must precede expression: - Usage: find [-H] [-L] [-P] [-Olevel] [-D [help|tree|search|stat|rates|opt|exec] [path...] [expression]

if I comment the sed line.

bash -x shows me the following executed command:

without the sed command:

find . -type f -not -name ''\''Text' - here - or - there*'\'''

with the sed command:

find . -type f -not -name ''\''Text\' '-\' 'here\' '-\' 'or\' 'there*'\'''

Is this even possible with find? I also tried escaping $find_list in the find command with no success.

Upvotes: 1

Views: 3179

Answers (2)

unconditional
unconditional

Reputation: 7656

Try this

#!/bin/bash

remove_except()
{
    local extension=$( printf "%q" "$1" )
    local dir=$( printf "%q" "$2" )
    local start_dir=$(pwd)

    [ -z "$extension" ] && return 1
    [ -z "$dir" ] || [ ! -d "$dir" ] && dir="."
    cd "$dir"

    local this="$0"
    this="${this##*/}"

    # exclude myself and extension
    local excludes=" -name \"$this\" -o -name \"*.$extension\" "

    for f in *."$extension";
    do
        filename="${f%.*}"
        excludes="$excludes -o -name \"$filename.*\""
    done

    eval "find . -maxdepth 1 -type f -not \( $excludes \) -print0" | xargs -0 -I {} rm -v {}

    cd "$start_dir"
}

remove_except "txt" "/your/dir"

Put into a script e.g. remove_except.sh and run it like this:

remove_except.sh "txt" "/your/dir"

The second argument is optional and will assume . if not specified.

Upvotes: -1

Charles Duffy
Charles Duffy

Reputation: 295403

Use an array, not a string.

#!/bin/bash
# ^-- must be /bin/bash, not /bin/sh, for this to work

excludes=( )
for filename in *.txt; do
  excludes+=( -not -name "${filename%.txt}" )
done

find . -type f -not -name '*.txt' "${excludes[@]}" -exec rm -f '{}' +

To understand why this works, see BashFAQ #50.


Now, if you want to be compatible with /bin/sh, not just bash, then encapsulate this in a function so you can overwrite the argument list (which is the only available array) without throwing away the script's global arguments:

delete_except_textfiles() {
  local filename 2>/dev/null ||: "local keyword not in POSIX, ignore if not present"
  set --
  for filename in *.txt; do
    set -- "$@" -not -name "${filename%.txt}"
  done
  find . -type f -not -name '*.txt' "$@" -exec rm -f '{}' +
}
delete_except_textfiles

Upvotes: 3

Related Questions