Intended task and limitations about PlistBuddy's -c switch

PlistBuddy shall print multiple entries of a given plist file at once (in this example Bookmarks.plist). Actually, the -c switch supports only one command per invocation. It does not support multiple commands in one shot (i. e. invoking PlistBuddy with a single -c containing multiple commands separated by coma or semicolon like in the invented example below):

# Invented command, don't try it, it doesn't work!

/usr/libexec/PlistBuddy -c 'Print :Entry1, Print :Entry2, Print :…'      # Multiple commands separated by comma doesn't work
/usr/libexec/PlistBuddy -c 'Print :Entry1; Print :Entry2; Print :…'      # Multiple commands separated by semikolon doesn't work either

In fact, every command (i. e. Print, Set, Add, Delete and so on) has to be invoked with its dedicated -c switch:

/usr/libexec/PlistBuddy -c 'Print :Entry1' -c 'Print :Entry2' -c 'Print :…'      # Propper invocation to get the values for Entry{1,2,…}

A real example for Bookmarks.plist looks like this:

/usr/libexec/PlistBuddy -c 'Print :Children:1:Children:1:URLString' -c 'Print :Children:1:Children:2:URLString' -c 'Print :Children:1:Children:3:URLString' Bookmarks.plist

Getting numerous entries from plist file this way would require invoking dozens or even hundreds of -c statements. Tedious!

Aspired solution: doing it programmatically using printf, brace expansion and xargs

My approach is to achieve this programmatically by combining printf, brace expansion {1..n} (ranges) and xargs.

The following line should do the whole magic. echo, i. e. invoke a dry-run, is used to check the proper syntax first:

printf -- "-c 'Print :Children:1:Children:%d:URLString' " {1..50} | xargs -0I{} echo /usr/libexec/PlistBuddy {} Bookmarks.plist

Perfect, the result is as expected:

/usr/libexec/PlistBuddy -c 'Print :Children:1:Children:1:URLString' -c 'Print :Children:1:Children:2:URLString' -c 'Print :Children:1:Children:3:URLString' -c 'Print :Children:1:Children:4:URLString' Bookmarks.plist

Let us examine the details for a better understanding

printf needs -- as first option in order to handle the leading hyphen from -c properly.
The range in braces {1..50} will be interpolated to 1, 2, 3, […], 50

The printf statement...

printf -- "-c 'Print :Children:1:Children:%d:URLString' " {1..50}

...will give us the following (interpolated) result:

-c 'Print :Children:1:Children:1:URLString' -c 'Print :Children:1:Children:2:URLString' […] -c 'Print :Children:1:Children:50:URLString' 

A closer look at the xargs part:

xargs -0I{} echo /usr/libexec/PlistBuddy {} Bookmarks.plist

Invoking xargs without any arguments takes a list from STDIN (one argument per line) and passes it (in groups) to another command. The main focus is that all values are appended (can be thought as appending a tail) at the end of command.

According to this thread:

-I option changes the way the new command lines are built. Instead of adding as many arguments as possible at a time, xargs will take one name at a time from its input, look for the given token ({} here) and replace that with the name.

The -0 option in your example instructs xargs to split its input on null bytes instead of blanks or newlines.

This is exactly what is needed; a kind of insertion between PlistBuddy and File and a proper handling of white spaces:


INSERTED COMMANDS is the place where all -c switches should be "inserted" by xargs.


Invoking this command without the echo throws the following error:

File Doesn't Exist, Will Create:  
-c 'Print :Children:1:Children:1:URLString' -c 'Print Children:1:Children:2:URLString' -c 'Print :Children:1:Children:3:URLString' -c 'Print :Children:1:Children:4:URLString'<br />

But invoking the result with copy and paste or piping it to a file and executing it as shell script...

printf -- "-c 'Print :Children:1:Children:%d:URLString' " {1..4} | xargs -0I{} echo /usr/libexec/PlistBuddy {}Bookmarks.plist > testing.sh && source ./testing.sh

... gives a list of bookmarks without any issues:


An even better workaround is to pipe the whole result to a shell:

printf -- "-c 'Print :Children:1:Children:%d:URLString' " {1..4} | xargs -0I{} echo /usr/libexec/PlistBuddy {}Bookmarks.plist | sh -

Yes, its a smart workaround, but it's still a workaround.


What is missing or how must the command look like to be properly executed within the shell (without the workaround of copy and paste or redirection into a second shell via pipe)?

  • 2
    If you don't get answers, please consider to edit this down to focus on the actual problem you currently have (without the background).
    – nohillside
    Commented Nov 5, 2021 at 15:58
  • Agreed - most of this needs to be an answer that may or may not get to the goal. The goal is really lost here. It should take maybe three sentences to describe clearly the problem - maybe a paragraph with exact "correct" results provided (a.k.a. test data) looks like. Then all the work in progress can be seen as an attempt to answer that needs more help
    – bmike
    Commented Feb 20 at 12:47

1 Answer 1


I may be misreading the question, but if your goal is to pull out some plist(5) data, have you considered going the xpath(1) way? For example, the following would try to first convert a Bookmarks.plist copy into XML and then traverse that tree looking for URLs in a hypothetical "Public" bookmarks folder:


# Copy the plist somewhere temporary.
cp ~/Library/Safari/Bookmarks.plist /tmp/Bookmarks.plist

# Use the property list utility to convert the copy to XML.
plutil -convert xml1 /tmp/Bookmarks.plist

# Filter for URLs saved in "Public".
xpath -q -e '/plist/dict/array/dict/key[.="WebBookmarkType"]/following-sibling::string[.="WebBookmarkTypeList"]/preceding-sibling::key[.="Title"]/following-sibling::string[contains(text(),"Public")]/parent::node()/array/dict/key[.="URLString"]/following-sibling::string[1]/text()' /tmp/Bookmarks.plist

You must log in to answer this question.

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