The following commands work

$ ls -1t | head -1
git_sync_log20180924_00.txt

$ vi git_sync_log20180924_00.txt

But this does not

$ ls -1t | head -1 | vi
Vim: Warning: Input is not from a terminal
Vim: Error reading input, exiting...
Vim: preserving files...
Vim: Finished.

How can I accomplish this (open most recently modified file in vi)?

up vote 13 down vote accepted
vi -- "$(ls -t | head -n 1)"

(that assumes file names don't contain newline characters).

Or if using zsh:

vi ./*(om[1])

Or:

vi ./*(.om[1])

To only consider regular files.

vi ./*(.Dom[1])

To also consider hidden files (as if using ls -At).

Those work regardless of what characters or byte values the file names may contain. For a GNU shell&utilities equivalent of that latter one, you can do:

IFS= read -rd '' file < <(
  find . ! -name . -prune -type f -printf '%T@\t%p\0' | sort -zrn | cut -zf2-) &&
  vi "$file"

With your pipeline structure, you could use xargs like so:

ls -1t | head -1 | xargs vi
  • 2
    That assumes file names don't contain blanks, newline, single quote, double quotes, backslashes and also means that vi's stdin will be either the pipe from head or /dev/null depending on the xargs implementation instead of the tty. – Stéphane Chazelas Sep 24 at 14:05
  • 1
    make that ... | xargs sh -c 'vi </dev/tty "$@"' sh; vi's input needs to be from a terminal. – mosvy Sep 24 at 14:08
  • @mosvy, or with GNU xargs and a shell with process substitution: xargs -r0d '\n' -a <(ls -t | head -n 1) vi -- – Stéphane Chazelas Sep 24 at 14:10

If you need a more solid solution that doesn’t rely on the flaky output of ls you can resort to stat(1). Most implementations have some way to specify a custom output format which can include timestamps to feed to sort(1) or an Awk script. Some examples:

  • with GNU coreutils:

    stat -L -c '%Y %n' -- *
    
  • with BSD coreutils:

    stat -L -t '%s' -f '%Sm %N' ./*
    

Subsequently you can sort and filter the result:

  • with coreutils only:

    stat ... | sort -t ' ' -k 1,1 -n -r | head -n 1 | cut -d ' ' -f 2-
    

    This requires O(n log n) time to sort the entire input even though you only need the maximum.

  • with Awk:

    stat ... | awk -F ' ' 'NR == 1 || $1 > m { m = $1; n = substr($0, length(m) + 2); } END{ if (NR) print(n); }'
    

    This runs in O(n) time because it only compares and updates the maximum.

  • How is that less flaky than using ls? That still assumes file names don't contain newline characters. You could make it work with the stat builtin of zsh (which predates both GNU and BSD stat) but then zsh has builtin support for for sorting file names by modification time, so you don't need stat at all there. Or by using --printf '%Y %n\0' with GNU stat and GNU sort -zrn. Also note that the GNU stat variant doesn't work for a file called -. – Stéphane Chazelas Sep 24 at 19:21

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.