开发者

Bash function to find newest file matching pattern

In Bash, I would like to create a function that returns the filename of the newest file that matches a certain pattern. For example, I have a directory of files like:

Directory/
   a1.1_5_1
   a1.2_1_4
   b2.1_0
   b2.2_3_4
   b2.3_2_0

I want the newest file that starts with开发者_如何学C 'b2'. How do I do this in bash? I need to have this in my ~/.bash_profile script.


The ls command has a parameter -t to sort by time. You can then grab the first (newest) with head -1.

ls -t b2* | head -1

But beware: Why you shouldn't parse the output of ls

My personal opinion: parsing ls is dangerous when the filenames can contain funny characters like spaces or newlines.

If you can guarantee that the filenames will not contain funny characters (maybe because you are in control of how the files are generated) then parsing ls is quite safe.

If you are developing a script which is meant to be run by many people on many systems in many different situations then do not parse ls.

Here is how to do it safe: How can I find the latest (newest, earliest, oldest) file in a directory?

unset -v latest
for file in "$dir"/*; do
  [[ $file -nt $latest ]] && latest=$file
done


The combination of find and ls works well for

  • filenames without newlines
  • not very large amount of files
  • not very long filenames

The solution:

find . -name "my-pattern" -print0 |
    xargs -r -0 ls -1 -t |
    head -1

Let's break it down:

With find we can match all interesting files like this:

find . -name "my-pattern" ...

then using -print0 we can pass all filenames safely to the ls like this:

find . -name "my-pattern" -print0 | xargs -r -0 ls -1 -t

additional find search parameters and patterns can be added here

find . -name "my-pattern" ... -print0 | xargs -r -0 ls -1 -t

ls -t will sort files by modification time (newest first) and print it one at a line. You can use -c to sort by creation time. Note: this will break with filenames containing newlines.

Finally head -1 gets us the first file in the sorted list.

Note: xargs use system limits to the size of the argument list. If this size exceeds, xargs will call ls multiple times. This will break the sorting and probably also the final output. Run

xargs  --show-limits

to check the limits on you system.

Note 2: use find . -maxdepth 1 -name "my-pattern" -print0 if you don't want to search files through subfolders.

Note 3: As pointed out by @starfry - -r argument for xargs is preventing the call of ls -1 -t, if no files were matched by the find. Thank you for the suggesion.


This is a possible implementation of the required Bash function:

# Print the newest file, if any, matching the given pattern
# Example usage:
#   newest_matching_file 'b2*'
# WARNING: Files whose names begin with a dot will not be checked
function newest_matching_file
{
    # Use ${1-} instead of $1 in case 'nounset' is set
    local -r glob_pattern=${1-}

    if (( $# != 1 )) ; then
        echo 'usage: newest_matching_file GLOB_PATTERN' >&2
        return 1
    fi

    # To avoid printing garbage if no files match the pattern, set
    # 'nullglob' if necessary
    local -i need_to_unset_nullglob=0
    if [[ ":$BASHOPTS:" != *:nullglob:* ]] ; then
        shopt -s nullglob
        need_to_unset_nullglob=1
    fi

    newest_file=
    for file in $glob_pattern ; do
        [[ -z $newest_file || $file -nt $newest_file ]] \
            && newest_file=$file
    done

    # To avoid unexpected behaviour elsewhere, unset nullglob if it was
    # set by this function
    (( need_to_unset_nullglob )) && shopt -u nullglob

    # Use printf instead of echo in case the file name begins with '-'
    [[ -n $newest_file ]] && printf '%s\n' "$newest_file"

    return 0
}

It uses only Bash builtins, and should handle files whose names contain newlines or other unusual characters.


Use the find command.

Assuming you're using Bash 4.2+, use -printf '%T+ %p\n' for file timestamp value.

find $DIR -type f -printf '%T+ %p\n' | sort -r | head -n 1 | cut -d' ' -f2

Example:

find ~/Downloads -type f -printf '%T+ %p\n' | sort -r | head -n 1 | cut -d' ' -f2

For a more useful script, see the find-latest script here: https://github.com/l3x/helpers


You can use stat with a file glob and a decorate-sort-undecorate with the file time added on the front:

$ stat -f "%m%t%N" b2* | sort -rn | head -1 | cut -f2-

As stated in comments, the best cross platform solution may be with a Python, Perl, Ruby script.

For such things, I tend to use Ruby since it is very awk like in the ease of writing small, throw away scripts yet has the power of Python or Perl right from the command line.

Here is a ruby:

ruby -e '
# index [0] for oldest and [-1] for newest
newest=Dir.glob("*").
    reject { |f| File.directory?(f)}.
    sort_by { |f| File.birthtime(f) rescue File.mtime(f) 
    }[-1]
p newest'

That gets the newest file in the current working directory.

You can also make the glob recursive by using **/* in glob or limit to matched files with b2*, etc


A Bash function to find the newest file under a directory matching a pattern

#1.  Make a bash function:
newest_file_matching_pattern(){ 
    find $1 -name "$2" -print0 | xargs -0 ls -1 -t | head -1  
} 
 
#2. Setup a scratch testing directory: 
mkdir /tmp/files_to_move;
cd /tmp/files_to_move;
touch file1.txt;
touch file2.txt; 
touch foobar.txt; 
 
#3. invoke the function: 
result=$(newest_file_matching_pattern /tmp/files_to_move "file*") 
printf "result: $result\n"

Prints:

result: /tmp/files_to_move/file2.txt

Or if brittle bash parlor tricks subcontracting to python interpreter is more your angle, this does the same thing:

#!/bin/bash 
 
function newest_file_matching_pattern { 
python - <<END 
import glob, os, re  
print(sorted(glob.glob("/tmp/files_to_move/file*"), key=os.path.getmtime)[0]); 
END 
} 
 
result=$(newest_file_matching_pattern) 
printf "result: $result\n" 

Prints:

result: /tmp/files_to_move/file2.txt


Unusual filenames (such as a file containing the valid \n character can wreak havoc with this kind of parsing. Here's a way to do it in Perl:

perl -le '@sorted = map {$_->[0]} 
                    sort {$a->[1] <=> $b->[1]} 
                    map {[$_, -M $_]} 
                    @ARGV;
          print $sorted[0]
' b2*

That's a Schwartzian transform used there.


For googlers:

ls -t | head -1

  • -t sorts by last modification datetime
  • head -1 only returns the first result

(Don't use in production)


There is a much more efficient way of achieving this. Consider the following command:

find . -cmin 1 -name "b2*"

This command finds the latest file produced exactly one minute ago with the wildcard search on "b2*". If you want files from the last two days then you'll be better off using the command below:

find . -mtime 2 -name "b2*"

The "." represents the current directory. Hope this helps.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜