I have a script that iterates through files and does some string substitution to insert the date.

f="/tmp/file.txt" # with .txt extension
timestamp="$(date +%H%M%S)"
echo "${f%%.*}-$timestamp.${f#*.}"

It provides the following output, which is correct.


But if a file doesn't have an extension, the script breaks.

f="/tmp/file" # no extension
timestamp="$(date +%H%M%S)"
echo "${f%%.*}-$timestamp.${f#*.}"
$ ./test.sh

Isn't it possible to use something like ${f:-not found} to fill in the blank if f is not defined? I can't figure out how to solve the above issue with the below method.

f="/tmp/file" # no extension
timestamp="$(date +%H%M%S)"
echo "${f%%.*}-$timestamp.${f#*.:-not found}"


$ ./test.sh

You'd also have problems with /tmp/dir.d/file files.

With zsh:

#! /bin/zsh -
f="/tmp/file.txt" # with .txt extension

set -o extendedglob
print -r -- ${f%$extension}-$timestamp$extension

With standard sh syntax (would also work in bash/zsh):

#! /bin/sh -
f="/tmp/file.txt" # with .txt extension
timestamp=$(date +%H%M%S)

case ${f##*/} in
  (*.*) printf '%s\n' "${f%.*}-$timestamp.${f##*.}";;
  (*)   printf '%s\n' "$f-$timestamp";;

With bash, you can replace timestamp=$(date +%H%M%S) with printf -v timestamp '%(%H%M%S)T' to avoid running that date command.


If you want a one-liner, here is a demo of a somewhat ineligant but workable solution:


# Test cases
# FILE="/tmp/file."
# FILE="/tmp/file.txt"
# FILE="/tmp/file.txt."

# TIMESTAMP="$(date +%H%M%S)"
printf -v TIMESTAMP '%(%H%M%S)T'

echo "${FILE%%.*}-${TIMESTAMP}.$([[ $FILE =~ \..*$ ]] && { echo "${FILE#*.}"; } || { echo "txt"; })"

The above adds .txt to the filename if missing an extension.

Use the following one-liner if you do not want a default extension added to a filename if missing an extension.

echo "${FILE%%.*}-${TIMESTAMP}$([[ $FILE =~ \..*$ ]] && { echo ".${FILE#*.}"; })"
  • @alecxs Because I got the impression that is what the OP wanted. Easily modified to something else or no extension at all. – fpmurphy Aug 10 '20 at 13:27
  • 1
    @alecxs. If the filename contains a trailing dot, it is preserved. – fpmurphy Aug 10 '20 at 15:18

to keep it simple

timestamp="$(date +%H%M%S)"
[ "${f%.*}" != "${f##*.}" ] && out="${f%.*}-${timestamp}.${f##*.}";
echo "$out"

to make it safe against . in path

[ "$dir" != "$file" ] && dir=${dir}/ || unset dir;
[ "$name" != "$suffix" ] && suffix=.${suffix} || unset suffix;
echo "${dir}${name}-${timestamp}${suffix}"

remove the newlines if you need it in one line
note: i recommend to use full timestamp (date + time) to ensure it is unique

  • however, that still fails for txt/txt or txt.txt – alecxs Aug 10 '20 at 9:40

It's not that f isn't defined, it's that ${f#*.} is the value of f with the the part until the first dot removed. If there is no dot, nothing is removed and ${f#*.} is just the same as $f. Also, since it matches up to the first dot, you get issues with paths like ./foo.txt (it gives -181548./foo.txt).

You'd need to either test if ${f#*.} equals $f or use something like ${f#${f%.*}}, which becomes empty if there is no dot-separated suffix. The counterpart for that would be ${f%.${f##*.}}. Though you'll still have issues with ./file again.

The problem with ./file suggests splitting the filename part from the path first, and proceeding from there.

An alternative approach would use regexes. This works for the filenames I tested:

timestamp="$(date +%H%M%S)"
if [[ $f =~ $re ]]; then
    echo "${BASH_REMATCH[1]}-${timestamp}${BASH_REMATCH[3]}"

That's (.*/)? -- optional path ending with a slash; [^/.]+ -- initial part of filename; (\.[^/]+)? -- optional extension.

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.