Running a command in each folder without affecting subfolders

I’d like to set the date and time stamp of all files in a given directory to the date and timestamp of the oldest file in that directory. It’s important that:

  1. nothing is processed in the root folder in which the command is invoked; and
  2. every folder is processed discretely without including its sub-folders in processing

In other words the idea is to walk the entire directory tree sub-directory by sub-directory and execute a command considering only the files in that particular directory, ignoring any children.

Finding the oldest file is a question of running:

ls -1t | tail -1

using that result with touch is a question of calling it like so:

touch -m -r $(ls -1t | tail -1)

Question is how to traverse the entire directory tree and call the above-mentioned within each directory such that every subdir is processed independently of its parent and children, meaning in the example below there would be 14 discrete calls to touch to set file date and time?

image

Is it as simple as issuing?:

for dir in */; do (cd “$dir” && touch -m -r $(ls -1t | tail -1) ‘{}’); done

1 Like

Here is a while loop and find command to traverse directories when using bash.

Bash script to traverse directories

#!/bin/bash
[ "$1" == "" ] && { echo "Usage: $0 dir-name"; exit 1; }

find $1 -mindepth 1 -maxdepth 1 -type d | while read -r dir
do
  pushd "$dir"  
  echo "$dir"
  # START - put your commands here like
 
 # touch -m -r $(ls -1t | tail -1)
  # ls 

  # END - No more commands and get back 
  popd
done

The find command used to deal with file names having a white space and other special characters. Here is an updated version with various fail safe options built into the script:

#!/bin/bash
[ "$1" == "" ] && { echo "Usage: $0 dir-name"; exit 1; }

find "$1" -mindepth 1 -maxdepth 1 -type d | while read -r dir
do
  pushd "$dir"  || exit 
  echo "$dir"
  # START - put your commands here like

  file="$(find .  -type f -printf '%T+ %p\n' | sort | head -1)"
  # NOTE: remove echo when everything works
  echo touch -m -r "$file"

  # END - No more commands and get back to dir
  popd || exit
done

Does this help?

You don’t need to bother quoting var assignments cuz they’re already implicitly quoted by the shell, enabling using double quotes inside the command substitutions for vars that need it.

I called it with . as parameter (i.e. start in current directory) and it throws the following:

touch: failed to get attributes of ‘2018-10-01+15:29:20.0000000000 ./file name.ext’: No such file or directory

Seems the script is appending ./ to filenames, which touch does not like.

I then tried running what I originally posted, and it works, except in that it creates an empty file named {} in every folder it traverses. I then modified it as follows, and it seems to do the trick:

for dir in */; do (cd "$dir" && touch -c -m -r "$(ls -1t | tail -1)" '{}'); done

-c causes touch to not create any files, and again it seems to work as intended and avoids creating the empty file {}.

Two questions if I may:

  1. does anyone see any problems with my approach?
  2. anyone able to explain what is triggering touch to want to create the empty file in each folder?

One problem I can see with my method is it bypasses hidden files.

@evand try passing full path like:

/script /path/to/dir 

Files with weird names will fail. For example This is a test file.txt as it contains blank spaces.

Why are using '{}' ? It is not needed. Remove it.

'{}' prevents shell doing something to it. There has been cases where even Bash will suddenly think it’s something that belongs to it despite being empty.

touch needs to know what files to operate on, if I’m not mistaken they’re passed via “{}”. Using “” also ensures that spaces etc. in filenames are not an issue.

running it without “{}” throws errors:

touch: missing file operand
Try ‘touch --help’ for more information.

whereas:

for dir in */; do (cd "$dir" && touch -c -m -r "$(ls -1t | tail -1)" "{}"); done

gets the job done, except it ignores hidden folders and files.

thanks @monk

Tried that, it throws the same error.

Here is fixed script:

#!/bin/bash
[ "$1" == "" ] && { echo "Usage: $0 dir-name"; exit 1; }

find "$1" -mindepth 1 -maxdepth 1 -type d | while read -r dir
do
  pushd "$dir"  || exit 
  echo "$dir"
  # START - put your commands here like

  file="$(find "$PWD"  -type f -printf '%T+ %p\n' | sort | head -1 | awk '{$1=""; print }' | sed 's/^[[:space:]]*//g')"
  #echo "Working in $PWD directory ..."
  # Make sure we work when file found
  # I used find to deal with hidden files too
  [ "$file" == "" ] || find "$PWD" -type f -print0 | xargs -I {} -0 touch -m -r "$file" "{}"

  # END - No more commands and get back to dir
  popd || exit
done

You can remove [ "$file" == "" ] as find will only work if file found. Not needed:

find "$PWD" -type f -print0 | xargs -I {} -0 touch -m -r "$file" "{}"

Thanks @monk, I think that solves it, much appreciated.

For learning, why did you elect to use awk and sed to derive the oldest filename over $(ls -1t | tail -1) ?

@evand No problem.

It is a terrible practice to parse ls command outputs. See “Why you shouldn’t parse the output of ls(1)” for a detailed explanation http://mywiki.wooledge.org/ParsingLs

1 Like

Yeah, don’t parse ls if you absolutely can’t avoid. for ... in ... and find are the ways.

@monk, before running that script across 100’s of folders I thought I’d set up a test folder and let it loose there.

It works, but not quite as I’d intended.


(apologies for dumping a single image like this, I was restricted to one image by the forum software so I screendumped the preview of the message I had tried to post

As you can see the script found the oldest file in the each branch of the subdirectory tree it was called from and used that as the target date and time for all files throughout that branch.

The results I want are as shown in the first image i.e. every single branch is treated independently of every other branch.

I’ve come up with an alternative that appears to work as intended:

#!/bin/bash

touch_dir() {
          cd "$1"
          # uncomment if you want to use file date and timestamps only
          oldest_file="$(find . -mindepth 1 -maxdepth 1 -type f -printf '%T+ %p\n' | sort | head -1 | awk '{$1=""; print }' | sed 's/^[[:space:]]*//g')"
          # uncomment if you want to use file and/or folder date and timestamps
          #oldest_file="$(find . -mindepth 1 -maxdepth 1 -printf '%T+ %p\n' | sort | head -1 | awk '{$1=""; print }' | sed 's/^[[:space:]]*//g')"

if [ ! -z "$oldest_file" ]

then

      echo "Processing "$PWD""
      # set date and timestamp of all files and parent folder using reference file
      find "$PWD" -mindepth 1 -maxdepth 1 -type f -print0 | xargs -I {} -0 touch -m -r "$oldest_file" "{}" 
      touch -m -r "$oldest_file" "$PWD"

fi
}

export -f touch_dir
find . -type d \( ! -name . \) -execdir bash -c 'touch_dir "$0"' {} \;   

Does anyone foresee any issue with this approach?

I’ve picked up some anomalies with this approach based on what looks like issues with certain folder and filenames. The error being thrown is:

“touch: failed to get attributes of ‘./filename.ext’: No such file or directory”

Looking at files that have thrown this error, they have one or more of the following attributes:

  • contain two consecutive spaces in the filename

I suspect that a space is being removed when deriving $oldest_file with the call to:
"$(find . -mindepth 1 -maxdepth 1 -type f -printf '%T+ %p\n' | sort | head -1 | awk '{$1=""; print }' | sed 's/^[[:space:]]*//g')"

Any insights appreciated.

That is because you used . in find instead of $PWD which gives full path. Hence, reaming commands worked but when you replaced with the ., you need to adjust the commands too:

find "$PWD"  -type f -printf '%T+ %p\n' | sort | head -1 | awk '{$1=""; print }' | sed 's/^[[:space:]]*//g'

Making that change makes no difference to the outcome.

#!/bin/bash

touch_dir() {
          cd "$1"
          # uncomment if you want to use file date and timestamps only
          oldest_file="$(find "$PWD" -mindepth 1 -maxdepth 1 -type f -printf '%T+ %p\n' | sort | head -1 | awk '{$1=""; print }' | sed 's/^[[:space:]]*//g')"
          # uncomment if you want to use file and/or folder date and timestamps
          #oldest_file="$(find "$PWD" -mindepth 1 -maxdepth 1 -printf '%T+ %p\n' | sort | head -1 | awk '{$1=""; print }' | sed 's/^[[:space:]]*//g')"

if [ ! -z "$oldest_file" ]

then

      echo "Processing "$PWD""
      # make sure hidden files are processed
      # shopt -s dotglob
      # set date and timestamp of all files and parent folder using reference file
      find "$PWD" -mindepth 1 -maxdepth 1 -type f -print0 | xargs -I {} -0 touch -m -r "$oldest_file" "{}" 
      # touch -m -r "$oldest_file" "*"
      touch -m -r "$oldest_file" "$PWD"

fi
}

export -f touch_dir
find "$PWD" -type d \( ! -name . \) -execdir bash -c 'touch_dir "$0"' {} \;

throws the same error.


Linux sysadmin blog - Linux/Unix Howtos and Tutorials - Linux bash shell scripting wiki