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
Sign up to request clarification or add additional context in comments.

1 Comment

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.
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 Comments

echo "$file", not echo $file; and echo "$f", not echo $f. See I just assigned a variable, but echo $variable shows something else!
(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!)
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
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

Comments

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".

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.