Suppose I have a subdirectory called sub. Then I want to operate on all files in that directory with the extension "txt". I want something like

for f in sub/*.txt
    enscript -b "" -o {$f basename excluding .txt}.ps sub/{$f basename excluding .txt}.txt                                                                                                                             
    ps2pdf {$f basename excluding .txt}.ps {$f basename excluding .txt}.pdf

So if there was just one file in the sub directory of the form *.txt, say sub/foo.txt, the script should expand to

enscript -b "" -o foo.ps sub/foo.txt                                                                                                                                   
ps2pdf foo.ps foo.pdf

I generally prefer portable solutions, but it is always interesting to learn about different ways of doing things. So non-portable is Ok, please just mention whether your solution is portable or non-portable. Also, the title is probably not the best possible, so feel free to modify.

EDIT: I realized the example as written did not quite make sense, since foo.txt was in the sub directory, and therefore sub/foo.txt would need to be passed to enscript. I also changed the -p enscript option to the more standard -o option.

2 Answers 2


POSIX shells (e.g., bash, dash, ksh, …) accept a ${var%suffix} construct that can be used to remove the trailing portion from the value of a variable. For instance, if path="sub/file.txt", then ${path%.txt} expands to sub/file.

There's a symmetric construct to remove a prefix: ${var#prefix}. The prefix can be a pattern. Doubling the # strips the longest matching prefix. For example, if path=sub/dir/file.txt, then ${path##*/} expands to file.txt (and ${path#*/} expands to dir/file.txt). The same goes for suffixes and %.

So, in your case you could write this (note that you can't combine the prefix stripping and the suffix stripping into a single expansion, at least not with only POSIX constructs):

for f in sub/*.txt; do
  enscript -b "" -o "$base.ps" "$f"
  ps2pdf "$base.ps" "$base.pdf"

Alternatively, the GNU basename command accepts an optional second argument which is the file extension to remove. For instance, if $f is file.txt, then $(basename $f .txt) expands to file.

Note, however, that basename removes all path information except for the last component, so if you want to remove just the extension you have to put that back (see the dirname command).

  • @Riccardo: Wow, that was fast. Thanks for replying. A comment and a question. 1) the link for basename has a typo (/ instead of .) and 2) these are links to what look like C utilities. Am I missing something? I should have said that my general preference for shell scripts is portability, so the ${path%.txt} will do me. I'm guessing the GNU basename thing, whatever it is. is not portable. Commented May 30, 2011 at 20:29
  • @Riccardo: I think your script has an error. Run exactly as written I get the error: syntax error near unexpected token prefix=${f%.txt}'. If I take out the semi-colon and add a do` it works. Commented May 30, 2011 at 20:42
  • @Gilles: You forgot to fix the basename url. :-) Commented May 30, 2011 at 21:00
  • @Faheem Mitha: Fixed the links. I guess the Gilles fixed the script already, as I can run it correctly? Commented May 30, 2011 at 21:03
  • @Riccardo: He did, yes. It just needed the addition of a do. Thanks. Commented May 30, 2011 at 21:12

The portable way is to strip the extension with ${f%.*} and the directory with ${f##*/} (or use basename).

In zsh, you can apply various transformations to parameter expansion, including history modifiers such as r to strip one extension and t to strip the directory part.

for x in sub/*.txt; do
    enscript -b "" -o ${x:t:r}.ps $x
    ps2pdf ${x:t:r}.ps ${x:t:r}.pdf

You can apply history modifiers to glob patterns as well.

for x in sub/*.txt(:r:t); do
    enscript -b "" -o $x.ps sub/$x.txt
    ps2pdf $x.ps $x.pdf
  • Like @Riccardo's version, this does not strip the leading sub/. As commented above, basename=${prefix#sub/} works, along the same lines as @Riccardo's approach. Commented May 31, 2011 at 8:11
  • well, zsh is clearly handy. Thanks. Commented May 31, 2011 at 11:53
  • I've made some corrections to my script in the question. Just mentioning it in case you feel like adjusting your answer. Commented May 31, 2011 at 16:36

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .