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?

Oortone
  • 175
  • 7
  • 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: https://askubuntu.com/a/889746 – jared_mamrot Nov 30 '21 at 21:27

4 Answers4

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
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • 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 Nov 30 '21 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 (_)

  • 1
    `echo "$file"`, not `echo $file`; and `echo "$f"`, not `echo $f`. See [I just assigned a variable, but `echo $variable` shows something else!](https://stackoverflow.com/questions/29378566/i-just-assigned-a-variable-but-echo-variable-shows-something-else) – Charles Duffy Nov 30 '21 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!) – Charles Duffy Nov 30 '21 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 – Guilherme Moreira Nov 30 '21 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
l0b0
  • 55,365
  • 30
  • 138
  • 223
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".
pjh
  • 6,388
  • 2
  • 16
  • 17