working with 'PlistBuddy'

hello,

i’ve been working with .plist files lately and needed to both add and delete a ‘dict’ entry nested in an array. needless to say, i’ve found little documentation on doing this and while i’ve found that adding with ‘/usr/bin/defaults’ works pretty well, i cannot seem to find an array entry and delete it. this may be just my inability to find the proper documentation.

in my search i discovered ‘PlistBuddy’, a mysterious program that is installed when ‘Automatic Updates’ are perfomed on a Macintosh. i’ve written a test script to find ‘PlistBuddy’ and use it to find an array entry and delete it. you can often find PlistBuddy installed in weird places like:

/Library/Receipts/AdditionalEssentials.pkg/Contents/Resources/PlistBuddy

i realize that the use could just ‘drag’ an icon off the Dock, and that this program is fairly useless. it’s real intention is as an example for anyone who may need the same kind of functionality i do. i have not found anything like it out on the web. i’ve tried to do my best to comment the code, but i’m certainly willing to answer any questions you may have.

here’s the code:


(* removeDockItem -- pulls your Dock items into a list and allows you to delete a single item.
    This is more a study on how to find and use PlistBuddy to remove items from a .plist file *)

(* Our properties.  We need to set myPlistBuddy to something so that we can test
    the variable after our 'Try' block.  The 'myPersistent' variables refer to 
    'arrays' in our .plist file.  You can check these with the Property List Editor,
    which is available when you install the Developer Tools (Xcode).  The global
    is so that we can always refer to the App or Other that the user picks to delete,
    and give good feedback *)
property myPlistBuddy : "no"
property myPersistentApps : "persistent-apps"
property myPersistentOthers : "persistent-others"
global myRm

(* This try block tests for PlistBuddy.  If PlistBuddy does not exist, we do _not_
    want our program to run.  Note that the 'sed' command effectively
    limits us to the _first_ instance of PlistBuddy that is found by 
    'locate'.  In my experience most users have several of these on
    their machines. *)
try
    set myPlistBuddy to (do shell script "/usr/bin/locate PlistBuddy | sed 2,10000d")
end try
if myPlistBuddy is "no" then
    display dialog "Cannot find PlistBuddy on this computer."
else
    doMain()
end if

(* If our 'Try' block worked, we can get into the main subroutine.  We'll let
    the user pick a category of alias to delete from the Dock, and use getArray()
    with the proper parameters.  We could probably subroutine the deletion as well,
    but it's just one line, so no big savings. *)
on doMain()
    set myButton to button returned of (display dialog "What would you like to remove from the Dock?" buttons {"Application", "Other", "Cancel"} default button "Application")
    if myButton is "Application" then
        set myDockApp to (getArray(myPersistentApps))
        if myDockApp ≥ 0 then
            do shell script myPlistBuddy & " -c \"delete " & myPersistentApps & ":" & myDockApp & " dict\" ~/Library/Preferences/com.apple.dock.plist"
            respawnDock()
            display dialog myRm & " has been deleted from the Dock"
        end if
    else if myButton is "Other" then
        set myDockOther to (getArray(myPersistentOthers))
        if myDockOther ≥ 0 then
            do shell script myPlistBuddy & " -c \"delete " & myPersistentOthers & ":" & myDockOther & " dict\" ~/Library/Preferences/com.apple.dock.plist"
            respawnDock()
            display dialog myRm & " has been deleted from the Dock"
        end if
    end if
end doMain

(* getArray() is where most of the work is done.  Regardless of the array we are
    working with, it does the same thing.  Our 'Repeat' block allows for an 'unlimited' 
    (heh!) number of Dock items, since we know that the text, "Does Not Exist" will be
    returned for non-existant elements.  We have an extra test for the "Dashboard",
    because for some reason this item is not named like the others.  We make up for this
    to avoid confusing the user. *)
on getArray(a)
    set myArray to {missing value}
    set x to 0
    repeat while (do shell script myPlistBuddy & " -c \"print " & a & ":" & x & "\" ~/Library/Preferences/com.apple.dock.plist") does not contain "Does Not Exist"
        if x is 0 then
            if (do shell script myPlistBuddy & " -c \"print " & a & ":" & x & ":tile-data:file-label" & "\" ~/Library/Preferences/com.apple.dock.plist") is "" and (do shell script myPlistBuddy & " -c \"print " & a & ":" & x & ":tile-type" & "\" ~/Library/Preferences/com.apple.dock.plist") is "dashboard-tile" then
                set myArray to "Dashboard" as list
            else
                set myArray to (do shell script myPlistBuddy & " -c \"print " & a & ":" & x & ":tile-data:file-label" & "\" ~/Library/Preferences/com.apple.dock.plist") as list
            end if
        else
            
            if ((do shell script myPlistBuddy & " -c \"print " & a & ":" & x & ":tile-data:file-label" & "\" ~/Library/Preferences/com.apple.dock.plist") is "") and ((do shell script myPlistBuddy & " -c \"print " & a & ":" & x & ":tile-type" & "\" ~/Library/Preferences/com.apple.dock.plist") is "dashboard-tile") then
                set myArray to (myArray & "Dashboard")
            else
                set myArray to (myArray & (do shell script myPlistBuddy & " -c \"print " & a & ":" & x & ":tile-data:file-label" & "\" ~/Library/Preferences/com.apple.dock.plist"))
            end if
        end if
        set x to (x + 1)
    end repeat
    set myRm to (choose from list myArray) as string
    set y to 1
    set z to -1
    -- This little 'Repeat' block searches the list we created above for the users selection.
    repeat while y ≤ length of myArray
        --display dialog item y of myArray
        if quoted form of item y of myArray is quoted form of myRm then
            set z to y
        end if
        set y to (y + 1)
    end repeat
    -- we return 'z - 1' because our array elements start at '0' but our list starts at '1'.
    if z ≥ 0 then
        return (z - 1)
    else
        return z
    end if
end getArray

-- respawnDock() is code i took from a Qwerty Denzel post.  Thanks Qwerty!
on respawnDock()
    tell application "Dock"
        quit
        repeat
            try
                activate
                exit repeat
            end try
        end repeat
    end tell
end respawnDock

NOTE: i’ve only tested this with 10.4.7. results with other versions of OS X would be informative.

here is another example of the same script. this one is a bit simpler. we are looking for ‘Global Login Items’, which are an Apple unapproved way of starting a service at login by any user. you’ll find this technique used by Symantec in particular.

since these are not named, as Dock items were above, we just pull a value pair that will make sense to us. the .plist we are looking at with this example is ‘/Library/Preferences/loginwindow.plist’. might i suggest you make a copy of this file before using the AppleScript below? it may save you some headaches to do so.

here is the code:


(* removeGlobalLoginItem -- removes startup items located in
	/Library/Preferences/loginwindow.plist *)

property myPlistBuddy : "no"
property myAutoLaunched : "AutoLaunchedApplicationDictionary"
global myRm

(* This try block tests for PlistBuddy.  If PlistBuddy does not exist, we do _not_
	want our program to run.  Note that the 'sed' command effectively
	limits us to the _first_ instance of PlistBuddy that is found by 
	'locate'.  In my experience most users have several of these on
	their machines. *)
try
	set myPlistBuddy to (do shell script "/usr/bin/locate PlistBuddy | sed 2,10000d")
end try
if myPlistBuddy is "no" then
	display dialog "Cannot find PlistBuddy on this computer."
else
	doMain()
end if

(* If our 'Try' block worked, we can get into the main subroutine.  We'll let
	the user pick a category of alias to delete from the Dock, and use getArray()
	with the proper parameters.  We could probably subroutine the deletion as well,
	but it's just one line, so no big savings. *)
on doMain()
	set myGlobalLoginItem to (getArray(myAutoLaunched))
	if myGlobalLoginItem ≥ 0 then
		do shell script myPlistBuddy & " -c \"delete " & myAutoLaunched & ":" & myGlobalLoginItem & " dict\" /Library/Preferences/loginwindow.plist"
		display dialog myRm & " has been deleted the Global Login Items list"
	end if
end doMain

(* getArray() is where most of the work is done.  Regardless of the array we are
	working with, it does the same thing.  Our 'Repeat' block allows for an 'unlimited' 
	(heh!) number of Global Login items, since we know that the text, "Does Not Exist" will be
	returned for non-existant elements. *)
on getArray(a)
	set myArray to {missing value}
	set x to 0
	repeat while (do shell script myPlistBuddy & " -c \"print " & a & ":" & x & "\" /Library/Preferences/loginwindow.plist") does not contain "Does Not Exist"
		if x is 0 then
			set myArray to (do shell script myPlistBuddy & " -c \"print " & a & ":" & x & ":Path" & "\" /Library/Preferences/loginwindow.plist") as list
		else
			set myArray to (myArray & (do shell script myPlistBuddy & " -c \"print " & a & ":" & x & ":Path" & "\" /Library/Preferences/loginwindow.plist"))
		end if
		set x to (x + 1)
	end repeat
	set myRm to (choose from list myArray) as string
	set y to 1
	set z to -1
	-- This little 'Repeat' block searches the list we created above for the users selection.
	repeat while y ≤ length of myArray
		if quoted form of item y of myArray is quoted form of myRm then
			set z to y
		end if
		set y to (y + 1)
	end repeat
	-- we return 'z - 1' because our array elements start at '0' but our list starts at '1'.
	if z ≥ 0 then
		return (z - 1)
	else
		return z
	end if
end getArray

hello,

recently, i needed to add a entry to a .plist file (/Library/Preferences/loginwindow.plist) and i specifically needed to add a ‘&’ at the end of my string entry. i needed this because when the program is started, i wanted it to be in the background. in other words, i did not want a Terminal window opening at every startup, just to annoy and be exited by my users.

to my chagrin, i have not been able to add a ‘&’ with the defaults command. here is one of my attempts:

/usr/bin/defaults write /Library/Preferences/loginwindow AutoLaunchedApplicationDictionary -array-add '<dict><key>Hide</key><true/><key>Path</key><string>/Library/Application Support/Path/To/Executable &</string></dict>'

i tried escaping it with a '', quoting it, etc. and was never able to get this working. however, with PlistBuddy i can easily do this.

NOTE: i’ve since found that you can do this by using ‘&’, like this:

/usr/bin/defaults write /Library/Preferences/loginwindow AutoLaunchedApplicationDictionary -array-add '<dict><key>Hide</key><true/><key>Path</key><string>/Library/Application Support/Path/To/Executable &</string></dict>'

first–i’ve created an AppleScript that is a bundle, and placed PlistBuddy in ‘/Contents/Resources’ (i could also use the technique outlined above). then, the script below:


property myAutoLaunched : "AutoLaunchedApplicationDictionary"
global myPlistBuddy

set myPlistBuddy to quoted form of POSIX path of (path to me) & "Contents/Resources/PlistBuddy"

on doMain()
    do shell script myPlistBuddy & " -c \"add " & myAutoLaunched & ":0 dict\" /Library/Preferences/loginwindow.plist"
    do shell script myPlistBuddy & " -c \"add " & myAutoLaunched & ":0:Hide bool true\" /Library/Preferences/loginwindow.plist"
    do shell script myPlistBuddy & " -c \"add " & myAutoLaunched & ":0:Path string '/Library/Application Support/Path/To/Executable  &'\" /Library/Preferences/loginwindow.plist"
end doMain

doMain()

for a script so short, it’s a bit convoluted, but it’s just adapted from the above scripts. i’m trying to keep things modular so that i can cut & paste & reuse. it adds a new entry at the beginning of the array (moving all other entries up one) and then adds my key values.

you can view the changes by opening Plist Editor (note that every time you make a change you must close and reopen the .plist), or by giving the defaults command below:

/usr/bin/defaults read /Library/Preferences/loginwindow

i hope this stuff is interesting and/or helps someone.

cheers.

EDITED: to add my note about &

Extremely interesting WaltR. I’m working on (but haven’t finished) a “watcher” script (basically a hidden on-idle stay-open folder-action kind of thing) and I’d like to make the preparation of the plist required absolutely transparent.

IF I can figure out how to, I’d like to write an ‘on wakeup’ handler based on launchd as well since I’d like certain scripts to run when I wake up my machine for the first time in a day, and a subset of those when I wake again it later. I’m sure I’ll be using variations on your scripts, so thanks.

I’m also interested (Craig Smith’s tutorial got me started) on a watcher for an external fw HD that I use for backup so that when I turned it on (I have a PowerKey Pro to do that) and it mounted, (watching /volumes/ for that), my backup routine would run automagically.