0

I've created two scripts. The first reads the mtime timestamps of all folders and writes it line by line to time_old.txt:

for d in */ ; do
    time_old=$(stat --format %Y "$d")
    escaped=$(printf %q "$d")
    echo "$time_old $escaped" >> time_old.txt
done

The second reads those lines back and runs touch with the old timestamps on the folders:

input="time_old.txt"
while IFS= read -r line
do
  echo "$line"
  touch_output=$(touch -m -d @$line)
  echo "$touch_output"
done < "$input"

This works for folders like Folder1, Folder2 but not for folders named Foo & Bar.

Where's my mistake?

lmoly
  • 407
  • Why don't you save the original timestamp on a separate empty file instead? touch -r original backupfile, do some work, and then restore with touch -r backupfile original; rm backupfile. That's easier and does not require parsing anything. – Kusalananda Apr 26 '22 at 12:08

2 Answers2

2

Instead, do (assuming GNU tools):

find . ! -name . -prune -type d -printf '@%T@\0%p\0' > time_old.list

And to restore:

xargs -t -a time_old.list -r0 -n2 touch -md

If you're using printf %q, you need to interpret the result as shell code. That could be:

find . ! -name . -prune -type d -printf 'touch -md @%T@ ' \
  -exec printf '%q\n' {} \; > time_old.bash

And:

bash -v ./time_old.bash

To restore.

But you need a standalone printf utility that supports that non-standard %q, and it to quote the file path in a format that is compatible with the shell language. If that's not guaranteed, that can become dangerous. That's also going to be a lot less efficient as you're forking and executing one process per directory (also the case with your GNU stat approach). I would not have code generated from external input interpreted by a shell if that can be avoided.

Also note that touch doesn't write anything on stdout, so the output=$(touch...) is pointless.

Here, you could also use zsh which has a safe quoting operator (using single-quotes which should be safe regardless of what character or non-character file names contain and be compatible with any sh-like shell (except yash)) and has a builtin stat (which actually predates GNU stat) so you don't have to rely on GNUisms.

zmodload zsh/stat

{ for d (*(ND/)) stat -A t -F @%s.%9. +mtime -- "$d" && print -r touch -d $t -- ${(qq)d} } > time_old.sh

And restore with sh -v ./time_old.sh.

1
escaped=$(printf %q "$d")
touch_output=$(touch -m -d @$line)

Where's my mistake?

You've arranged for the filename to be quoted in the file, but the problem is that quotes only work if they're straight on the shell command line, the results of expansions are not parsed for quotes, backslash-escapes or other shell syntax. Instead, those characters are treated literally. So if $line is 1650819409 Foo\ \&\ Bar, it'll get word-split to 1650819409, Foo\, \&\, and Bar. This is similar to the issues in How can we run a command stored in a variable? (You could use eval to have the shell parse the expanded value, but that has its own set of issues, esp. wrt. untrusted modifications to the data.)


If you're happy with assuming that your filenames can't contain newlines, you could just store them raw. This would create lines like 1650819409 Foo & Bar, where everything after the first space is the filename.

> time_old.txt
for d in */ ; do
    case "$d" in
        *$'\n'*) printf >&2 "newlines in filenames not supported, skipping %q\n" "$d"; continue;;
    esac
    time_old=$(stat --format %Y -- "$d") &&
      printf '%s\n' "$time_old $d"
done > time_old.txt

input="time_old.txt" while IFS= read -r line do time=${line%% } # remove from first newline to end file=${line# } # remove up to and incl. the first newline printf >&2 "Changing time on '%s' to '%s'\n" "$file" "$time" touch -m -d "@$time" -- "$file" done < "$input"

ilkkachu
  • 138,973