What's wrong
for filename in $(ls -1 ./*); do
Ouch! As a general rule (with very few exceptions), do not use ls
in scripts. What you wrote is almost equivalent to for filename in ./*; do
, except that if there are any non-printable characters, whitespace characters, or \[?*
in the file names, they will be mangled if you use ls
. You don't need the ./
(except to ensure that the file name doesn't start with a -
) but it doesn't hurt.
[[ $filename =~ _[0-9]\. ]]
could be written in a slightly simpler and more portable way: [[ $filename = *_[0-9].* ]]
. And since you can use a simple glob pattern, you might as well not have iterated over all file names: for filename in *_[0-9].*; do
. But there's a better way to express your script, to take advantage of BASH_REMATCH
; see below.
echo $filename
Always put double quotes around variable and command substitutions. Exception: when you understand why you need to leave the double quotes off and why it's safe to do so. When the shell sees a variable substitution ($foo
or ${foo}
) or a command substitution (`foo`
or $(foo)
) outside double quotes, the result of the substitution undergoes word splitting and globbing (filename generation). That was one of the problems with $(ls -1 ./*)
earlier. This should be echo "$filename"
.
In fact, it would be better printf "%s" "$filename"
, because echo
itself performs expansions. In bash, unless you've set non-default options to enable backslash expansion, the only problem is that a few arguments beginning with a -
look like option, and in this specific case the filename will begin with ./
.
There is an edge case where your sed call won't work: if you have a file name that ends with a newline character. This doesn't happen in practice unless someone has made a mistake (like a rogue script or a bad copy-paste) or is deliberately trying to trick your script — so watch out in security contexts.
Incidentally, this is one of the few cases where it's safe to leave out the double quotes around a command substitution: in a variable assignment, there is an implicit pair of double quotes around the right-hand side, so new_filename=$(…)
is equivalent to new_filename="$(…)"
. Note that this does not extend to export VARIABLE="$(value)"
, where the double quotes are necessary.
We now turn to your question about the use of sed
. It is not necessary here; you can perform this substitution in bash. Bash has a pattern replacement parameter substitution feature, but it's limited to a constant replacement text. Bash also has a way to extract substrings from regexp matches with =~
, through the BASH_REMATCH
variable. After the match, ${BASH_REMATCH[0]}
contains the portion of the string matched by the regexp, ${BASH_REMATCH[1]}
contains the portion matched by the first parenthesized group and so on.
mv $filename $new_filename
Again, double quotes.
A working script
One possibility is to extract the text to replace from the regexp match, then perform a string replacement on it. Since I'm not using ./*
, the file name may begin with a -
, so I take care to use --
on the call to mv
to ensure that the file name isn't seen as an option.
for filename in *; do
if [[ $filename =~ _[0-9]\. ]]; then
from=${BASH_REMATCH[0]}
mv -- "$filename" "${filename//$from/${from/_/_0}}"
fi
done
Another possibility is to match the whole file name as a regexp and splice a 0
into the bits. Note that this will behave differently in a corner case: if there are several occurrences of _[0-9]\.
, the code above replaces the first occurrence, while this replaces the last occurrence.
for filename in *; do
if [[ $filename =~ ^(.*_)([0-9]\..*)$ ]]; then
mv -- "$filename" "${BASH_REMATCH[1]}0${BASH_REMATCH[2]}"
fi
done