1

(This is a simplest working example of something in a much messier context.)

I have a directory d with files 1.txt 2.txt 3.txt 4.wav. My current directory is somewhere else.

ls d reports 1.txt 2.txt 3.txt 4.wav, as it should.

But I want only 1.txt 2.txt 3.txt, so I try ls d/*.txt.

But that prepends a d/ to each filename: d/1.txt d/2.txt d/3.txt, because the shell expanded the * before ls saw it.

To get my desired 1.txt 2.txt 3.txt, I could ls d | grep txt, or ls d/*.txt | xargs basename -a, or even spawn a new shell: (cd d; ls *.txt). Is there a better way, less clunky, less vulnerable to gotchas and corner cases, to filter the output of ls?

  • If find command is available, then checkout stackoverflow.com/questions/5456120/… – Vikyboss May 4 '16 at 20:14
  • 1
    So, find d -name \*.txt -printf "%f\n" ? Yes, that works. Even though it shuffles the filenames, I'd accept it as an answer if you post it. – Camille Goudeseune May 4 '16 at 20:23
  • @CamilleGoudeseune, you can also post your answer and accept it too. If stackexchange doesn't allow, then let me know. – Vikyboss May 4 '16 at 21:14
2

I believe that the simplest way to do what you ask is:

$ ( cd d; ls *.txt )
1.txt  2.txt  3.txt

that is happening inside a sub-shell ( ... ) so the directory change is not permanent, is valid only for the execution of the two commands.

A more robust version is:

$ ( cd d && ls -d -- *.txt )
1.txt  2.txt  3.txt

In which ls is not executed unless the directory change was successful, and ls lists directories instead of their content and doesn't take files whose name starts with a dash as on option.


If you don't mind changing the positional parameters ($1, $2, etc.):
This is also relatively simple:

$ set d/*.txt
$ for f do printf '%s\n' "${f#d/}"; done
1.txt
2.txt
3.txt

This will work in POSIX shells:

$ dash -c 'set d/*.txt; for f do printf "%s\n" "${f#d/}"; done'
1.txt
2.txt
3.txt

You could use GNU find's -printf:

$ find d -maxdepth 1 -iname "*.txt" -printf "%f\n" | sort
1.txt
2.txt
3.txt

But that is getting complex enough. Sorry, there is no more "simple" solutions AFAIK.

  • This "GNU find" solution improves on Vikyboss's comment. Nice! – Camille Goudeseune May 5 '16 at 16:23
3

With zsh:

printf '%s\n' d/*.txt(:t)

:t like for csh history modifiers but here in a glob qualifier, gets you the tail of the file name.

Also:

files=(d/*.txt)
printf '%s\n' $files:t

In other Bourne-like shells, you could always do:

(cd d && printf '%s\n' *.txt)

Note that it doesn't fork a new shell, it creates a subshell environment. In most shell implementations, that subshell environment is implemented by forking a child process, but a new shell is not executed in there (it's not a new shell, it's the same shell forked in a new process). Also note that if the last command in the subshell is an external command, most shells (not bash though) will not fork an extra process, so the total number of processes run will be the same as without the sub-shell environment.

ksh93 doesn't fork for subshells. It does it by undoing the modifications done within the subshell upon exiting from it. So, there (where printf is also built-in), (cd d && printf '%s\n' *.txt) doesn't fork extra processes.

Also note that ls lists the files and content of directories it is being passed as arguments. Here, you don't need ls if it's just for printing the names given by the shell, but if you insist in using ls, you should pass the -d option so it doesn't list the content of directories:

(cd d && ls -d -- *.txt)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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