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 '17 at 15:48
  • Please, could you post an desired output? Or is your goal to rename files? – John Goofy Jul 14 '17 at 16:15
  • @JohnGoofy please note my edit from ~30 min ago and the answer that Sundeep gave. – ylluminate Jul 14 '17 at 16:31

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 '17 at 16:08
  • 0 is starting index... so the first - index is 4... – Sundeep Jul 14 '17 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. – BallpointBen Jul 14 '17 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 '17 at 15:52
  • You can e.g. pipe the output to another command. – FloHe Jul 14 '17 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 '17 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. – dave_thompson_085 Jul 15 '17 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 '17 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). – steeldriver Jul 14 '17 at 17:46

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.