7

I frequently download PDF files with heinous numeric file names from my browser. These automatically go into ~/Downloads. Ideally I would like to just be able to open these files with:

evince "the most recently modified file"

without having to open ~/Downloads to find the file name.

Is there a simple way to specify "the most recently modified file" in bash?

NOTE: I know that it is possible to do this, but ideally I am looking for a solution that would be simpler than ls -t'ing ~/Downloads to check the name.

DQdlM
  • 1,543
  • 1
    There is no way to do this with bash that is both simple and foolproof. I believe zsh can do this with the *(om[1]) glob. – jw013 Mar 22 '12 at 15:11

5 Answers5

10
evince "$(ls -t | head -n1)"

While it will handle spaces, tabs, and (I believe) printing specials, it will break if the filename contains a newline, and possibly on some other non-printing characters.

Kevin
  • 40,767
5

This is a more correct and robust approach than ls -t, at the cost of some additional complexity.

Setup

Add a short shell script (code below) to your $PATH. ~/bin is a good place for it.

Remember to make sure

  • the script is executable chmod +x ~/bin/script_name
  • ~/bin is in your $PATH

Usage

Pass the command you want to run on the newest file in ~/Download to last_download.

Examples

Assuming you named the script last_download

  • last_download (no arguments): runs evince, the default command, on the newest file in ~/Downloads
  • last_download mplayer: runs mplayer on the newest file in ~/Downloads
  • last_download cp -t ~/Desktop: copies the newest file in ~/Downloads to ~/Desktop

Code

#!/bin/sh
# Usage: last_download [cmd [options]...]

newest=
dir=~/Downloads

# default command
if [ $# -eq 0 ]; then
        set -- evince;
fi

# find newest file
for f in "$dir"/*; do
        if [ -z "$newest" ] || [ "$f" -nt "$newest" ]; then
                newest="$f"
        fi
done
if ! [ -e "$newest" ]; then
        exit 1
fi

# run command on newest file
"$@" "$newest"

Note: The script only looks in ~/Download but it would not be hard to generalize it to support any directory, in which case a name change would also be warranted.

jw013
  • 51,212
  • Well done, nice and thorough. – Kevin Mar 22 '12 at 16:01
  • 1
    ... except it puts the command to run outside your shell context, so it won't work with bash builtin commands, aliases, functions, anything set up in your bashrc, etc. – peth Mar 22 '12 at 16:48
  • @user112553 Yes that is a good point. However, it would be trivial to convert the script into a function if the user so desired. – jw013 Mar 22 '12 at 17:32
  • True. I just saw everyone else nitpicking and wanted my own nit. ;) – peth Mar 22 '12 at 17:56
  • @jw013 Sorry to ask (I'm a novice) but, how would you convert the script into a function so that it will recognize your aliases, etc? I tried: function last_file { source ~/scripts/last_file.sh }

    At the end of my .bashrc, but aliases were not preserved.

    – user1247 Mar 31 '12 at 12:49
  • @user1247 I would just copy paste it instead of sourcing it, since you have to tweak it anyways. last_file () { #copy paste code here }. The only change to make is to change the exit to return, so the function doesn't exit your shell. – jw013 Mar 31 '12 at 13:01
  • @jw013 Hmm, still the exact same behavior; it works but doesn't recognize aliases (I did source my .bashrc don't worry). ETA: I made a test script: alias echohello="echo hello"; function test() { echohello }, which works fine. So it has something to do with how your script handles input – user1247 Mar 31 '12 at 13:28
  • @user1247 Sorry I got distracted from the bigger picture. The fundamental problem is aliases are expanded when functions are read, not when they are executed. The function expands its arguments to obtain the command to run, so passing in aliases won't work. The solution is simple: change your aliases to functions, which do work, since functions supersede aliases anyways. – jw013 Mar 31 '12 at 19:52
  • @jw013 Thanks, I read somewhere else that that is a good idea, so I'll do it, even though I have a lot of aliases :) – user1247 Mar 31 '12 at 19:57
  • @jw013 Actually, it still doesn't work correctly. If I give the last_file () function a function as a command, it doesn't run it. The function exits fine, and correctly finds the most recent file, but it does not run the command I give it. I can't figure out what is going wrong :( ... you might want to just try it out yourself, as the copy-paste job will only take a sec. – user1247 Mar 31 '12 at 20:13
  • @user1247 type set -x (turn on xtrace) and try again and see what it tells you. – jw013 Mar 31 '12 at 20:16
  • @jw013 OK, this tells me that some of the aliases I have turned into functions are not taking arguments! For example: function cdtest () { cd } When I run "cdtest Downloads/" on the command line xtrace shows that only the command "cd" is run. On the other hand another function: function e() { emacs } Does work on when given arguments on the command line!!?? Is the difference that 'cd' is a bash builtin? But in either case xtrace shows the last_file () given the command 'e' or 'cdtest' runs only 'emacs' or 'cd' without the argument given to it. – user1247 Apr 01 '12 at 09:05
  • @user1247 You have to make all your functions look like, e.g. cdtest () { cd "$@"; }, and then they should work with arguments. – jw013 Apr 01 '12 at 09:36
  • thanks for this answer, it is probably more than I need but there is a lot to learn here! – DQdlM Apr 04 '12 at 14:06
  • If something happens in a subfolder, this subfolder may be newer than the last real file. So I enclosed the inner if by if [ -f "$f" ]; then ... fi
  • It should probably be put into ~/.local/bin
  • To make it applicable to the current folder replace dir=~/Downloads by dir=\pwd` 4) It is very comfortable to replace the default action byls` to quickly check which file will get the command
  • – Rainer Glüge Apr 30 '20 at 13:57