7

I work on testcase, each of being a subdirectory of ~/test. It's convenient to see which were the testcases I worked most recently using something like ls -rtl. Now I'd like to remove certain temporary files from the test directories; alas, that would change the timestamp of the directories.

Is it possible to remove a file from a directory without changing the directory's modification timestamp?

1
  • can the temporary files be placed somewhere else? because the filename is part of the directory, not the file itself, so when a filename goes away...
    – thrig
    May 22, 2018 at 20:20

3 Answers 3

4

You'll have to reset the timestamp on the directory after removing the files. Assuming GNU tools, something like this should work:

mtime=$(stat -c %y dir)            # get the timestamp, store in $mtime
rm dir/somefile dir/someotherfile  # do whatever you need
touch -d "$mtime" dir              # set the timestamp back

That resets the modification (mtime) and access (atime) timestamps on the directory to the original modification timestamp, but also sets the change timestamp (ctime) to the current time. Changing the ctime is unavoidable, but you probably don't care about it or atime.

0
3

The touch command provides the -r option which allows you to touch a file with the same timestamps as a reference file. As such, you could do this:

touch -r /dir/somefile /tmp/_thetimestamps  # save the timestamps in /tmp/_thetimestamps
rm /dir/somefile                            # do whatever you need
touch -r /tmp/_thetimestamps /dir           # set the timestamps back

Linux Man:

-r, --reference=FILE
      use this file's times instead of current time

Unix Man:

-r       Use the access and modifications times from the specified file
         instead of the current time of day.
-1

Here is the Python version of ikkachu's excellent answer:

https://gist.github.com/jaytaylor/ea1b6082ea5bcea7a6b65518d91238f5

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Removes a file or directory while preserving the modification time (mtime) of
the parent directory.

Pure-python implementation.

See also:
    https://unix.stackexchange.com/a/565595/22709
"""

import argparse
import logging
import os
import platform
import re
import shutil
import stat
import sys

log_formatter = logging.Formatter('%(asctime)s, %(levelname)s %(message)s')

logging.basicConfig(level=logging.INFO, formatter=log_formatter)
logger = logging.getLogger(__name__)

def parse_opts(args):
    parser = argparse.ArgumentParser(
        description='Removes a file or directory while preserving the modification time (mtime) of the parent directory.',
        argument_default=False,
    )
    parser.add_argument('-f', '--force', default=False, action='store_true', help='Ignore nonexistent files and arguments')
    parser.add_argument('-i', '--interactive', default=False, action='store_true', help='Prompt for confirmation before taking action on a target')
    parser.add_argument('--no-preserve-root', default=False, action='store_true', help="Do not treat '/' specially")
    parser.add_argument('-r', '--recursive', default=False, action='store_true', help='Remove directories and their contents recursively')
    parser.add_argument('-v', '--verbose', default=False, action='store_true', help='Display verbose log output')
    parser.add_argument('paths', type=str, nargs='+', help='Filesystem path(s) to remove')

    opts = parser.parse_args(args)
    if opts.verbose:
        logger.setLevel(logging.DEBUG)

    return opts

# n.b. Use the appropriate input function depending on whether the runtime
#      environment is Python version 2 or 3.
_input_fn = input if sys.version_info > (3, 0) else raw_input

def _prompt_for_confirmation(path, opts):
    if not opts.interactive:
        return True

    response = _input_fn('Permanently remove "%s"? (Y/n)' % (path,))
    return response in ('Y', 'y')

def require_write_permissions(path):
    parent = os.path.dirname(path)
    if not os.access(parent, os.W_OK):
        raise Exception('Missing necessary write permission for parent directory "%s"; operation aborted' % (parent,))

def rm_preserving_parent_mtime(path, opts):
    """
    Deletes the specified path, and restores the filesystem access and modified
    timestamp values afterward removing path.

    IMPORTANT
    ---------
    Take note of the permission test before removing the target. These checks
    verify write access to parent's parent.

    Without the check, there is a risk that the file will be removed and then
    setting mtime fails.  When this happens, the entire purpose of this program is defeated.
    """
    if not opts.no_preserve_root and (path == os.sep or (platform.system() == 'Windows' and re.match(r'^[A-Z]:%s?' % (os.sep,), path))):
        raise Exception('Cowardly refusing to operate on root path')

    if path in ('', '.', '..'):
        raise Exception('Invalid path "%s", must have a parent directory' % (path,))

    parent = os.path.dirname(path)

    if path == parent:
        raise Exception('Invalid path, parent directory="%s" should not equal path="%s"' % (parent, path))

    st = os.stat(parent)
    atime = st[stat.ST_ATIME]
    mtime = st[stat.ST_MTIME]

    modified = False

    try:
        if os.path.isfile(path):
            require_write_permissions(parent)
            if _prompt_for_confirmation(path, opts):
                logger.debug('Removing file "%s"' % (path,))
                modified = True
                os.remove(path)
        elif os.path.isdir(path):
            if not opts.recursive:
                raise Exception('Cannot remove "%s": Is a directory' % (path,))
            require_write_permissions(parent)
            if _prompt_for_confirmation(path, opts):
                logger.debug('Removing directory "%s"' % (path,))
                modified = True
                shutil.rmtree(path)
        else:
            raise Exception('Path "%s" is not a file or directory' % (path,))
    finally:
        if modified:
            logger.debug('Restoring access and modification timestamps for parent="%s"' % (parent,))
            os.utime(parent, (atime, mtime))

def main(paths, opts):
    try:
        for path in paths:
            rm_preserving_parent_mtime(path, opts)
        return 0
    except BaseException:
        logger.exception('Caught exception in main')
        if opts.force:
            return 0
        return 1

if __name__ == '__main__':
    opts = parse_opts(sys.argv[1:])
    sys.exit(main(opts.paths, opts))

For complete solutions for moving or deleting files while preserving parent directory mtime, see these python scripts: https://gist.github.com/jaytaylor/e2e0b53baf224f4e973b252370499de7

You must log in to answer this question.

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