I have a bunch of files in the following format:

2014-12-03.1. could be anything.ext
2014-11-26 613 adva.ext

My goal is to iterate over the entire list of files and to take the date formatting from YYYY-MM-DD and store that in a variable in the format of YYYYMMDD for further processing (in my case It's going to be pushed into a touch command).

So normally I would match against this regular expression: (\d{4})-(\d{2})-(\d{2}).*

And then use $1$2$3 to get my desired pattern, however I'm not sure how to do this in bash / zsh.

How can this be done within a shell script as such?

  • @Sundeep that latter option is better wrt parameter expansion. So how does that work precisely? Right now in your example you get the YYYY and MM, but then you just grab the rest with ${f:8}, when I would rather just grab DD and discard .* (everything after DD).
    – ylluminate
    Jul 14, 2017 at 15:48
  • Please, could you post an desired output? Or is your goal to rename files?
    – John Goofy
    Jul 14, 2017 at 16:15
  • @JohnGoofy please note my edit from ~30 min ago and the answer that Sundeep gave.
    – ylluminate
    Jul 14, 2017 at 16:31

6 Answers 6


Using parameter expansion

$ touch 2014-11-19.8.ext 2014-11-26.1.ext
$ for f in *.ext; do d="${f:0:4}${f:5:2}${f:8:2}"; echo "$d"; done
  • ${f:0:4} means 4 characters starting from index 0 and f is variable name
  • replace echo "$d" with your code
  • Interesting, so the "cursor" is on the index and is not inclusive, but rather exclusive. so in this case 5:2 starts at the 1st dash, but does not include it. 8:2 starts at the 2nd dash and does not include it. Very interesting and great to know.
    – ylluminate
    Jul 14, 2017 at 16:08
  • 0 is starting index... so the first - index is 4...
    – Sundeep
    Jul 14, 2017 at 16:09
  • I always think of indices as pointing between characters instead of at them. The oddball case is then requesting a single character, in which case the index is short for "between that index and the subsequent index", i.e. the character right of that index. Jul 14, 2017 at 18:27

To loop over every file in the current directory and compare their filenames to the desired pattern, then set a variable containing the date pieces

for f in *
  [[ $f =~ ^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])(.*) ]] && 

This uses bash's [[ ability to use regular expression matching to place the date pieces into the BASH_REMATCH array.


You can do it interactively by using GNU sed:

$ sed 's/^\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}.*\)/\1\2\3/g' stuff.txt

For multiple files (if in same directory and no other considered files in directory):

for file in *
    if [ -f "$file" ]
          sed 's/^\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\).*/\1\2\3/g' "$file"
  • Okay, nice, but I'm personally liking the conciseness of the comments that @Sundeep is leaving above where you seem to have more readable control over the fields. My goal here is to extract out these elements and then use them in another command (specifically I'm setting times via touch). Not quite sure why he's not starting it of as an answer instead of comments...
    – ylluminate
    Jul 14, 2017 at 15:52
  • You can e.g. pipe the output to another command.
    – FloHe
    Jul 14, 2017 at 16:01

Here is a zsh way of doing this, without loops:

autoload -U zmv
zmv -n '([0-9](#c4))-([0-9](#c2))-([0-9](#c2))(*)' '$1$2$3$4'
  • [0-9](#c4) means any digit repeated 4 times
  • $1-$2 refer to previously used parenthesis
  • -n prevents execution (only prints), remove this flag if you are happy with the result

As zsh takes care of globbing all corner cases (whitespaces, special characters, etc) should be taken automatically into account.


If you're on GNU Coreutils, you have this:

$ date --date=2014-11-13 +"%Y%m%d"


$ date --date=2014-11-130ABCJUNK +"%Y%m%d"
date: invalid date ‘2014-11-130ABCJUNK’

So the task is much simpler: extact the first ten characters of each YYYY-MM-DDetc filename to get the date by itself, then pass to date for reformatting.

But, if we are on GNU Coreutils, we can skip the date command because touch has the exact same --date=STRING option.

for file in * ; do
  date=${file%${file##??????????}} # chop all but first ten
  touch --date=$date -- "$file"

But why do this ten character chopping in the POSIX portable way when we are relying on touch to be from GNU Coreutils?

for file in * ; do
  touch --date=$date -- "$file"
  • I was told by someone that touch required YYYYMMDD only format when the -t parameter was issued...
    – ylluminate
    Jul 15, 2017 at 3:08
  • @ylluminate: -t requires [[cc]yy]mmddhhmm[.ss] -- which is not the same as you wrote, although it does omit punctuation other than possibly one dot -- but in the GNU version (as clearly stated) --date (or -d) is different. Jul 15, 2017 at 9:09

Try pattern substitution:


parameter is the basename of the file. pattern is a dash. In this /- to replace pattern globally. string is empty, since you want delete the dashes.

mv "${f}" "${f//-/}"

Caveat: I didn't get this working with the case of spaces in the extention.

  • So in this case I'm wanting to not only delete the dashes, but everything after the DD in the pattern. The goal is to extract YYYYMMDD to use in a touch command following getting the correct pattern as I'm going to leave the filename itself intact and just pull out the dating for filesystem timestamp setting. Might see above comments by @Sundeep so far as they're close, but I'm not following the actual logic so far in the parameter expansion chain for the end.
    – ylluminate
    Jul 14, 2017 at 15:55
  • @ylluminate you could do a two-step substitution like for f in *.ext; do d="${f%%.*}"; echo "${d//-}"; done though, (first remove the longest trailing string, then remove the dashes). Jul 14, 2017 at 17:46

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .