2

Doing some reading here and here I found this solution to replace two underscores in filenames with only one using bash:

for file in *; do
  f=${file//__/_}
  echo $f
done;

However how do I most easily expand this expression to replace an arbitrary number of underscores with only one?

1
  • 1
    If you have extended globbing (enable it using shopt -s extglob if it's not already enabled) you could use f="${f//+(_)/_}". E.g. for the file "test_____something__else.txt", for f in test_*; do f="${f//+(_)/_}"; echo $f; done returns test_something_else.txt. More details here: askubuntu.com/a/889746 Commented Nov 30, 2021 at 21:27

4 Answers 4

4

Typically, it's going to be faster to just put your original code in a loop than to do anything else.

for file in *; do
  f=$file
  while [[ $f = *__* ]]; do
    f=${f//__/_}
  done
  echo "$f"
done

Even better, if you're on a modern shell release, you can enable extended globs, which provide regex-like functionality:

shopt -s extglob
for file in *; do
  f=${file//+(_)/_}
  echo "$f"
done
1
  • Great, number 2 really made things easier. Being a sporadic bash user I find it very hard to figure out these things just by searching manuals and descriptions.
    – Oortone
    Commented Nov 30, 2021 at 22:04
0

You could use a simple regex using sed

for file in *; do
  f=$(echo "$file" | sed -e 's/_\+/_/')
  echo "$f"
done;

This regex matches one or more underscores (_\+) and substitutes them with only one (_)

3
  • 1
    echo "$file", not echo $file; and echo "$f", not echo $f. See I just assigned a variable, but echo $variable shows something else! Commented Nov 30, 2021 at 21:30
  • 1
    (also, this is going to be a lot slower than using the OP's original code in a loop; starting a pipeline for every file is expensive!) Commented Nov 30, 2021 at 21:31
  • Edited my answer to correct the variable usage, and you are absolutely correct about it being slower. I wouldn't use this in any directory with a lot of files Commented Nov 30, 2021 at 21:45
0

GNU tr has --squeeze-repeats:

$ echo foo_______bar | tr --squeeze-repeats _
foo_bar

If you're using BSD tr you can use -s instead:

$ echo foo_______bar | tr -s _
foo_bar
0

This Shellcheck-clean pure shell code should work with any POSIX-compliant shell, including bash and dash:

for file in *; do
    while :; do
        case $file in
            *__*)   file=${file%%__*}_${file#*__};;
            *)      break;;
        esac
    done
    printf '%s\n' "$file"
done
  • ${file%%__*} expands to $file with the first __ and all characters after it removed (e.g. a__b__c produces a).
  • ${file#*__} expands to $file with all characters up to and including the first __ removed (e.g. a__b__c produces b__c).
  • See the accepted, and excellent, answer to Why is printf better than echo? for an explanation of why printf '%s\n' "$file" is used instead of echo "$file".

Not the answer you're looking for? Browse other questions tagged or ask your own question.