0

CONTEXT: Catalina MacOS: A set of .pdf files is in a directory. Some of the filenames have spaces (problematic with xargs.

From the command line: the goal is to return for each .pdf file, the filename and the number of pages.

This snippet returns a list filenames that could be pipelined into xargs:

find . -type f -name  '*.pdf'

These this snippet returns the number of pages:

pdfinfo foo.pdf | grep Pages | awk '{print $2}' 


pdftk foo.pdf dump_data | grep Pages | awk '{print $2}'

How can the snippets be used with xargs to only process .pdf files that may have spaces in their filenames?

Failed:

 find . -name '*.pdf' | xargs pdfinfo | 

Snippet prints filename (though unable to handle names with spaces) and does not print page number on the same line as filename:

find . -name '*.pdf' | xargs -I % sh -c 'echo %;  pdfinfo % | grep Pages'
0

4 Answers 4

2

Some of the filenames have spaces (problematic with xargs.

You just need to use the -print0 option for find, and the -0 option for xargs. They should really advertise these very much at the top of their man pages!

find -iname '*.pdf' -print0 | xargs -0 ...

just works. These options tell find to separate found filenames with a zero byte instead of a newline character. Unlike spaces, newlines, colons, …, zero bytes are not allowed within file names, so this is a safe way to separate file names. -0 tells xargs to expect zero bytes as dividers (and nothing else).

However, this is OS X, you got good shells going probably; so, no need for find at all.

#!/usr/bin/zsh -

for pdffile in **/*.pdf(N-.) ; do
   print -r -- "${pdffile}" # This is already problematic again. Your file names
                            # might contain newlines, spaces etc, so no easy way
                            # to tell where file name ends and page count starts
   pdfinfo -- "${pdffile}" | grep Pages | awk '{print $2}'
done

Note the double quotation marks " where you tried to use ': Your code cannot work, because '-encased string doesn't undergo variable expansion, so the string is passed as-is (including the dollar sign and variable name) to the program being called. Which is what you want for the awk argument, because you need to pass $, but not where you actually want to expand the content of your variable.

Note that fuzzy drawing's answer is right, you can absorb the grep into the awk call. You should also make sure the regex is as precise as possible.

Let's also fix the "file names can contain spaces and newlines and numbers, so in my output I won't be able to tell where file names start and end" issue, by ourselves generating 0-delimited output:

#!/usr/bin/zsh

for pdffile in **/*.pdf(N-.) ; do
   pages=$(pdfinfo -- "${pdffile}" | awk '/^Pages:/{print $2}')
   printf '%s\0\%d\0' "${pdffile}" "${pages}"
done

(you could still have problems with PDF files whose Creator or Producer contains <newline>Pages:, but at least, by using that strict regex above, we've minimised the risk).

3
  • 2
    No need for xargs and the -print0/-0 GNU extensions. You can use the standard find ... -exec cmd {} + syntax here. Mar 23, 2022 at 11:20
  • I forget that these are GNU extensions! Mar 23, 2022 at 13:25
  • Note though that although they (like -iname) started as GNU extensions, they are supported by many more implementations including those of most if not all BSDs including macos. They're still not standard though and not many non-GNU text utilities can deal with nul-delimited records or lines that contain nuls. Mar 23, 2022 at 13:29
1

You can use find's -exec command to run pdfinfo then pipe the result to awk which can do pattern-matching on its own without requiring grep as an intermediate step:

find . -type f -name '*.pdf' -exec pdfinfo '{}' \; | awk '/Pages/ {print $2}'

Of course that only gives the number of pages and I now see that for each file you want both the filename and total pages. I don't think xargs will help here, but a while loop will do the job:

#!/bin/sh
find . -type f -name '*.pdf' | while read -r f; do
    p=$(pdfinfo "$f" | awk '/Pages/ {print $2}')
    printf '%s\n' "$f $p"
done
2
  • be careful, file names can also contain newlines. Mar 23, 2022 at 6:12
  • @MarcusMüller OP mentioned spaces only so I didn't think newlines were a concern. Mar 23, 2022 at 15:49
1

If you don't need to go down a directory tree, this for loop might do:

for FN in *pdf; do pdfinfo "$FN" | awk '/^Pages/ {print ARGV[2], $2; exit}' - "$FN"; done
1
  • Any comments / explanation would be helpful to the novice.
    – gatorback
    Mar 23, 2022 at 23:27
1

With exiftool:

exiftool -r -ext pdf -q -p '$PageCount $Directory/$Filename' .

-r (for recursive) in combination with -ext pdf would do something similar to find . -name '*.pdf'.

That's good for display.

For a post-processable output, like in a shell loop, you'd rather want to use some NUL-delimited output format:

exiftool -r -ext pdf -q -if 'print "$PageCount/$Directory/$Filename\0";0' . |
  while IFS=/ read -rd '' page file; do
    something with "$page" and "$file"
  done

(assuming zsh or bash -O lastpipe)

Or some of the serialisation formats it supports such as json, xml or php¹:

$ exiftool -r -ext pdf -q -j -PageCount .
[{
  "SourceFile": "./a.pdf",
  "PageCount": 4
},
{
  "SourceFile": "./a\nb.pdf",
  "PageCount": 4
}]
$ exiftool -r -ext pdf -q -X -PageCount .
<?xml version='1.0' encoding='UTF-8'?>
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>

<rdf:Description rdf:about='./a.pdf'
  xmlns:et='http://ns.exiftool.ca/1.0/' et:toolkit='Image::ExifTool 11.88'
  xmlns:PDF='http://ns.exiftool.ca/PDF/PDF/1.0/'>
 <PDF:PageCount>4</PDF:PageCount>
</rdf:Description>

<rdf:Description rdf:about='./a
b.pdf'
  xmlns:et='http://ns.exiftool.ca/1.0/' et:toolkit='Image::ExifTool 11.88'
  xmlns:PDF='http://ns.exiftool.ca/PDF/PDF/1.0/'>
 <PDF:PageCount>4</PDF:PageCount>
</rdf:Description>
</rdf:RDF>
exiftool -r -ext pdf -q -php -PageCount .
Array(Array(
  "SourceFile" => "./a.pdf",
  "PageCount" => 4
),
Array(
  "SourceFile" => "./a\nb.pdf",
  "PageCount" => 4
));

(here with a filename containing a newline character as example).


¹ beware however that the JSON and XML ones only work for filenames made of text properly encoded in UTF-8, a limitation of those formats.

You must log in to answer this question.

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