1

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

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

It provides the following output, which is correct.

/tmp/file-220304.txt 

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

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

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.

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

Results:

$ ./test.sh /tmp/file-221420./tmp/file 
0

    4 Answers 4

    1

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

    With zsh:

    #! /bin/zsh - f="/tmp/file.txt" # with .txt extension timestamp=${(%):-%D{%H%M%S}} set -o extendedglob extension=${(M)f%.[^./]#} 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";; esac 

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

      1

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

      #!/bin/bash # Test cases FILE="/tmp/file" # 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#*.}"; })" 
      2
      • @alecxs Because I got the impression that is what the OP wanted. Easily modified to something else or no extension at all.
        – fpmurphy
        CommentedAug 10, 2020 at 13:27
      • 1
        @alecxs. If the filename contains a trailing dot, it is preserved.
        – fpmurphy
        CommentedAug 10, 2020 at 15:18
      0

      to keep it simple

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

      to make it safe against . in path

      dir=${f%/*}; file=${f##*/}; name=${file%.*}; suffix=${file##*.}; [ "$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

      1
      • however, that still fails for txt/txt or txt.txt
        – alecxs
        CommentedAug 10, 2020 at 9:40
      0

      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:

      #!/bin/bash f=$1 timestamp="$(date +%H%M%S)" re='^((.*/)?[^/.]+)(\.[^/]+)?$' if [[ $f =~ $re ]]; then echo "${BASH_REMATCH[1]}-${timestamp}${BASH_REMATCH[3]}" fi 

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

        You must log in to answer this question.

        Start asking to get answers

        Find the answer to your question by asking.

        Ask question

        Explore related questions

        See similar questions with these tags.