Is there any way to substitute text in variables on several patterns at time or even using back reference?

For example, I have FILE=filename.ext and I want to change it to filename_sometext.ext. But I don't know that file extension is .ext. All I know about it is that extension is after last dot. So I can do it in two steps:


Can I do it on one step (something like ${FILE/.\(*\)/_sometext.\1} [that doesn't work])?

By the way I need to do it in pure shell without sed/awk/etc. My shell is ksh, but if there is way to do it with bashisms I'd like to know it too.

  • I assume you mean ksh88 not ksh93? – Mikel May 11 '12 at 8:55
  • What if the file name contains multiple dots? – Mikel May 11 '12 at 9:13
  • I mean ksh93. Does it matter so much? And extension is after last dot, it doesn't matter if multiple dots appear before. – rush May 11 '12 at 9:49
  • Yes, it matters, ksh88 is what most people mean when they say ksh, and it doesn't support many features that are specific to ksh93. Hence why I asked. ;-) – Mikel May 11 '12 at 15:20

Bash Parameter Expansion says that the variable (FILE in your example) must be a parameter name. So they don't nest. And the last part of ${param/pattern/replacement} must be a string. So back references aren't supported.

My only advice is to use


to avoid adding a trailing dot if the file has no extension.


Apparently back references are supported in ksh93.

So you could use something like

  • I got information about back reference and nested substitution. Thank you. – rush May 11 '12 at 9:51
  • 1
    Your ${FILE/@(.*)/_something\1} would be like ${FILE/./_something.} and change the first occurrence of .. You'd need ${FILE/%.*([^.])/_something\0} or ${FILE/@(*)./\1_something.} to match on the last one like ${FILE%.*} would. – Stéphane Chazelas Oct 19 '15 at 8:52

In this particular case I think FILE=${FILE%.*}_sometext.${FILE##*.} would do the job.

  • Yeah, it did the job, but I think my example was a little wrong for that. – rush May 11 '12 at 9:47


In any shell, you could concatenate variables or values:

$ a=One; b=Two; c=Three
$ d="$a$b$c"; echo $d

The ${parameter%word} type of structures is POSIX have been supported in most shells for a long time.
Assuming FILE=filename.ext, you could do:

$ FILE="${FILE%.*}_sometext.${FILE##*.}"; echo "$FILE"

That is one line (as requested) and works on most shells.

It is also possible to use variables (even with several dots):

echo "EXT=$EXT"
echo "final FILE=${FILE%.*}${ADDTEXT}.$EXT"

Or, in just one line:

echo "one   FILE=$ONEFILE"

Pattern substitution

What gets tricky is trying to use ${parameter/pattern/string} «Pattern substitution.»

A variable works in many shells (even busybox ash):

echo "ash   FILE=${FILE/.*/${NEWTEXT}}"

But not in older shells.

Only for FILE=filename.ext:
This works in bash (and ksh, ksh93, mksh, zsh) but not in ash, dash, sh or csh

echo "bash  FILE=${FILE/%.*/${ADDTEXT}.${FILE##*.}}"

Note that it is using the % character to indicate a match at the end of the parameter (but, sadly it is greedy and eats all dots in FILE=file.one.name.ext).


The best method to control how greedy is the substitution is to use separate variables (as opposed to some from of «Pattern substitution.»)


If the shell variable $FILE contains a . the above command will reassign to it the old value less everything following the last . then _sometext and last the old value's last . and everything after.

And so:

printf %s\\n "$FILE"


If the shell variable $FILE does not contain a dot, then the _sometext string is merely appended to the end of the old value:

printf %s\\n "$FILE"


This is importantly different behavior than you might get if you did not nest the parameter expansion as I do on the end there:

printf %s\\n "$FILE"


When you nest the parameter expansion it is evaluated inside to outside, and so the first thing that happens is:

for    FILE in    some.dot no_dots_at_all
do     printf %s\\n '${FILE#'"${FILE%.*}"'}'


...the shell removes any matched bits and uses the remains as the pattern to strip from the outer expansion. And so when there aren't any matched bits and the variable expands in full the outer expansion strips away all of the variable rather than substituting it into your expression twice.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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