views:

1647

answers:

7

Does anybody have an Xcode script for generating @property and @synthsize directives for instance variables in a class?

+2  A: 

This is the one I came up with based on one I found a long time ago, rewritten in Python and with the improvements that it can generate multiple properties at once, among other things.

It will generate properties for all selected instance variable using (copy) as the attribute.

There are still some edge cases with multiple @interfaces or @implementations in a file, as well as some with unusual identifiers or asterisk placement (as in *const), but it should cover most typical coding styles. Feel free to edit/post modifications if you fix any of these cases.

#!/usr/bin/python

# Takes a header file with one or more instance variables selected
# and creates properties and synthesize directives for the selected properties.

# Accepts google-style instance variables with a tailing underscore and
# creates an appropriately named property without underscore.

# Entire Document
# Home Directory
# Discard Output
# Display in Alert

import os
import re
import subprocess

# AppleScripts for altering contents of files via Xcode
setFileContentsScript = """\
on run argv
  set fileAlias to POSIX file (item 1 of argv)
  set newDocText to (item 2 of argv)
    tell application "Xcode"
      set doc to open fileAlias
      set text of doc to newDocText
    end tell
end run \
"""

getFileContentsScript = """\
on run argv
  set fileAlias to POSIX file (item 1 of argv)
    tell application "Xcode"
      set doc to open fileAlias
      set docText to text of doc
  end tell
  return docText
end run \
"""

# Get variables from Xcode
headerFileText = """%%%{PBXAllText}%%%"""
selectionStartIndex = %%%{PBXSelectionStart}%%%
selectionEndIndex = %%%{PBXSelectionEnd}%%%
selectedText = headerFileText[selectionStartIndex:selectionEndIndex]

headerFilePath = """%%%{PBXFilePath}%%%"""

# Look for an implementation file with .m or .mm extension
implementationFilePath = headerFilePath[:-1] + "m"
if not os.path.exists(implementationFilePath):
    implementationFilePath += "m"

instanceVariablesRegex = re.compile(
  """^\s*((?:(?:\w+)\s+)*(?:(?:\w+)))""" + # Identifier(s)
  """([*]?)\\s*""" + # An optional asterisk
  """(\\w+?)(_?);""", # The variable name
  re.M)

# Now for each instance variable in the selected section
properties = ""
synthesizes = ""

for lineMatch in instanceVariablesRegex.findall(selectedText):
    types = " ".join(lineMatch[0].split()) # Clean up consequtive whitespace
    asterisk = lineMatch[1]
    variableName = lineMatch[2]
    trailingUnderscore = lineMatch[3]

    pointerPropertyAttributes = "(copy) " # Attributes if variable is pointer
    if not asterisk:
      pointerPropertyAttributes = ""

    newProperty = "@property %s%s %s%s;\n" % (pointerPropertyAttributes,
                                             types,
                                             asterisk,
                                             variableName)

    # If there's a trailing underscore, we need to let the synthesize
    # know which backing variable it's using
    newSynthesize = "@synthesize %s%s;\n" % (variableName,
                                           trailingUnderscore and
                                           " = %s_" % variableName)

    properties += newProperty
    synthesizes += newSynthesize

# Check to make sure at least 1 properties was found to generate
if not properties:
  os.sys.stderr.writelines("No properties found to generate")
  exit(-1)

# We want to insert the new properties either immediately after the last
# existing property or at the end of the instance variable section
findLastPropertyRegex = re.compile("^@interface.*?{.*?}.*?\\n" +
                                   "(?:.*^\\s*@property.*?\\n)?", re.M | re.S)
headerInsertIndex = findLastPropertyRegex.search(headerFileText).end()

# Add new lines on either side if this is the only property in the file
addedNewLine = "\n"
if re.search("^\s*@property", headerFileText, re.M):
  # Not the only property, don't add
  addedNewLine = ""

newHeaderFileText = "%s%s%s%s" % (headerFileText[:headerInsertIndex],
                                addedNewLine,
                                properties,
                                headerFileText[headerInsertIndex:])

subprocess.call(["osascript",
                "-e",
                setFileContentsScript,
                headerFilePath,
                newHeaderFileText])


if not os.path.exists(implementationFilePath):
  os.sys.stdout.writelines("No implementation file found")
  exit(0)

implementationFileText = subprocess.Popen(
  ["osascript",
   "-e",
  getFileContentsScript,
   implementationFilePath],
  stdout=subprocess.PIPE).communicate()[0]

# We want to insert the synthesizes either immediately after the last existing
# @synthesize or after the @implementation directive
lastSynthesizeRegex = re.compile("^\\s*@implementation.*?\\n" +
                                "(?:.*^\\s*@synthesize.*?\\n)?", re.M | re.S)

implementationInsertIndex = \
  lastSynthesizeRegex.search(implementationFileText).end()

# Add new lines on either side if this is the only synthesize in the file
addedNewLine = "\n"
if re.search("^\s*@synthesize", implementationFileText, re.M):
  # Not the only synthesize, don't add
  addedNewLine = ""

newImplementationFileText = "%s%s%s%s" % \
                  (implementationFileText[:implementationInsertIndex],
                   addedNewLine,
                   synthesizes,
                   implementationFileText[implementationInsertIndex:])

subprocess.call(["osascript",
                 "-e",
                 setFileContentsScript,
                 implementationFilePath,
                 newImplementationFileText])

# Switch Xcode back to header file
subprocess.Popen(["osascript",
                  "-e",
                  getFileContentsScript,
                  headerFilePath],
                 stdout=subprocess.PIPE).communicate()
Lawrence Johnston
Uh, isn't it "synthesize", with two e's? I'm assuming this script can't possibly work properly with it spelled "synthsize".
n8gray
Yes, thanks for pointing that out.
Lawrence Johnston
A: 

Here's one I wrote up yesterday to do the @property directives before coming across this question a few hours later. It's a simple text filter and would be trivial to extend it to @synthesize directives (add an appropriate when clause to the case statement and make appropriate additions to the when block_end condition), and not much more work to extend it to handle multiple occurrences of @interface/@implementation in one file (by tracking their names --- it can be done through regexp captures, as everything else is in the script):

#! /usr/bin/ruby

# -------------- Basic Definitions -----------------------------

doc = "%%%{PBXFilePath}%%%"

# regular expressions

search_exp = /[[:space:]]*([[a-zA-Z0-9]]*)[[:space:]]\*([a-zA-Z0-9]*)/
interface_start = /@interface/
block_end = /^\}/

#initializing variables

properties_list = []
properties_string = ""
reading_interface = 0

#---------------- Start Processing -----------------------------

file = File.open(doc, "r").readlines

file.each do |line| 

# capture the regular expression matches only in the 
# interface declaration and print out the matching
# property declarations

  case line

  # start capturing
  when interface_start
    reading_interface = 1
    puts line

  # capture and keep in properties_list
  when search_exp
    if (reading_interface == 1) then
      data = Regexp.last_match
      properties_list <<  data
    end
    puts line

  # unpack properties_list and print out the property
  # declarations
  when block_end
    if (reading_interface == 1) then
      reading_interface = 0
      properties_list.each do |pair|
        properties_string << "@property (readwrite, copy) #{pair[0].lstrip};\n"
      end
      puts line
      puts "\n" + properties_string
    end
  else puts line
  end

end

I run this using "no input" and "replace document contents" as the options for I/O in the User Scripts editor.

Pinochle
+4  A: 

I use Accessorizer which does this and a whole lot more.

http://www.kevincallahan.org/software/accessorizer.html

very inexpensive and powerful.

Dad
Thanks for the suggestion — I'll take a look.
Lawrence Johnston
+1  A: 

Here is the userscript I currently use - it works on one instance variable at a time. It tries to use the right retain mechanism (simple types are not retained), and it also creates the @synthesize statement in the implementation file - currently it does not yet create dealloc statements for you.

#! /usr/bin/perl -w

#Input: Selection
#Directory: Selection
#Output: Display in Alert
#Errors: Display in Alert

use strict;

# Get the header file contents from Xcode user scripts
my $headerFileContents = <<'HEADERFILECONTENTS';
%%%{PBXAllText}%%%
HEADERFILECONTENTS

# Get the indices of the selection from Xcode user scripts
my $selectionStartIndex = %%%{PBXSelectionStart}%%%;
my $selectionEndIndex = %%%{PBXSelectionEnd}%%%;

# Get path of the header file
my $implementationFilePath = "%%%{PBXFilePath}%%%";
my $headerFilePath = $implementationFilePath;

# Look for an implemenation file with a ".m" or ".mm" extension
$implementationFilePath =~ s/\.[hm]*$/.m/;
if (!(-e $implementationFilePath))
{
    $implementationFilePath =~ s/.m$/.mm/;
}

# Handle subroutine to trime whitespace off both ends of a string
sub trim
{
    my $string = shift;
    $string =~ s/^\s*(.*?)\s*$/$1/;
    return $string;
}


# Get the selection out of the header file
my $selectedText =  substr $headerFileContents, $selectionStartIndex, ($selectionEndIndex - $selectionStartIndex);

#my $otherText = substr $headerFileContents, $selectionStartIndex;
#my $pulledText = "";
#if ( length($otherText) && $otherText =~ /.*$(^.*;).*/ )
#{
#    $pulledText = $1;
#}
#
#
#print $pulledText;


$selectedText = trim $selectedText;


my $type = "";
my $asterisk = "";
my $name = "";
my $behavior = "";
my $iboutlet = "";

# Test that the selection is:
#  At series of identifiers (the type name and access specifiers)
#  Possibly an asterisk
#  Another identifier (the variable name)
#  A semi-colon
if (length($selectedText) && ($selectedText =~ /([_A-Za-z][_A-Za-z0-9]*\s*)+([\s\*]+)([_A-Za-z][_A-Za-z0-9]*)/))
{
    $type = $1;
    $type = trim $type;
    $asterisk = $2;
    $asterisk = trim $asterisk;
    $name = $3;
    $behavior = "";
    if (defined($asterisk) && length($asterisk) == 1)
    {
        $behavior = "(nonatomic, retain) ";
    }
    else
    {
        $behavior = "(nonatomic) ";
        $asterisk = "";
    }
}
else
{
    print "Bailing, error in Regex";
    exit 1;
}

# special case, see if we need to keep around an IBOUTLET declaration.
if ( length($selectedText) && ($selectedText =~ /IBOutlet/) )
{
   $iboutlet = "IBOutlet ";
}

# Find the closing brace (end of the class variables section)
my $remainderOfHeader = substr $headerFileContents, $selectionEndIndex;
my $indexAfterClosingBrace = $selectionEndIndex + index($remainderOfHeader, "\n}\n") + 3;
if ($indexAfterClosingBrace == -1)
{
    exit 1;
}

# Determine if we need to add a newline in front of the property declaration
my $leadingNewline = "\n";
if (substr($headerFileContents, $indexAfterClosingBrace, 1) eq "\n")
{
    $indexAfterClosingBrace += 1;
    $leadingNewline = "";
}

# Determine if we need to add a newline after the property declaration
my $trailingNewline = "\n";
if (substr($headerFileContents, $indexAfterClosingBrace, 9) eq "\@property")
{
    $trailingNewline = "";
}

# Create and insert the proper declaration
my $propertyDeclaration = $leadingNewline . "\@property " . $behavior . $iboutlet . $type . " " . $asterisk . $name . ";\n" . $trailingNewline; 
substr($headerFileContents, $indexAfterClosingBrace, 0) = $propertyDeclaration;

my $replaceFileContentsScript = <<'REPLACEFILESCRIPT';
on run argv
    set fileAlias to POSIX file (item 1 of argv)
    set newDocText to (item 2 of argv)
    tell application "Xcode"
        set doc to open fileAlias
        set text of doc to newDocText
    end tell
end run
REPLACEFILESCRIPT

# Use Applescript to replace the contents of the header file
# (I could have used the "Output" of the Xcode user script instead)
system 'osascript', '-e', $replaceFileContentsScript, $headerFilePath, $headerFileContents;

# Stop now if the implementation file can't be found
if (!(-e $implementationFilePath))
{
    exit 1;
}

my $getFileContentsScript = <<'GETFILESCRIPT';
on run argv
    set fileAlias to POSIX file (item 1 of argv)
    tell application "Xcode"
        set doc to open fileAlias
        set docText to text of doc
    end tell
    return docText
end run
GETFILESCRIPT

# Get the contents of the implmentation file
open(SCRIPTFILE, '-|') || exec 'osascript', '-e', $getFileContentsScript, $implementationFilePath;
my $implementationFileContents = do {local $/; <SCRIPTFILE>};
close(SCRIPTFILE);

# Look for the class implementation statement
if (length($implementationFileContents) && ($implementationFileContents =~ /(\@implementation [_A-Za-z][_A-Za-z0-9]*\n)/))
{
    my $matchString = $1;
    my $indexAfterMatch = index($implementationFileContents, $matchString) + length($matchString);

    # Determine if we want a newline before the synthesize statement
    $leadingNewline = "\n";
    if (substr($implementationFileContents, $indexAfterMatch, 1) eq "\n")
    {
        $indexAfterMatch += 1;
        $leadingNewline = "";
    }

    # Determine if we want a newline after the synthesize statement
    $trailingNewline = "\n";
    if (substr($implementationFileContents, $indexAfterMatch, 11) eq "\@synthesize")
    {
        $trailingNewline = "";
    }

    # Create and insert the synthesize statement 
    my $synthesizeStatement = $leadingNewline . "\@synthesize " . $name . ";\n" . $trailingNewline;
    substr($implementationFileContents, $indexAfterMatch, 0) = $synthesizeStatement;

    # Use Applescript to replace the contents of the implementation file in Xcode
    system 'osascript', '-e', $replaceFileContentsScript, $implementationFilePath, $implementationFileContents;
}

exit 0;
Kendall Helmstetter Gelner
A: 

Accessorizer http://www.kevincallahan.org/software/accessorizer.html does this stuff and a lot more. It also handles custom prefixes and postfixes (suffixes). If you want Google's underscore, you got it. If you want to change it, change it on the fly - no need to edit scripts. Further, there's a defaults table where you can define default property specifiers based on the type of ivar passed in (copy, retain, readonly, assign etc) . It does IBOutlet detection and automatically inserts the IBOutlet keyword, nils out your views for -viewDidUnload, does several styles of dealloc. It also writes all those hairy accessors for collections (NSMutableArray and NSSet). It does key-archiving, various locking approaches, it can sort your property and synthesize blocks, write KVO code, Singleton code, convert to selector, generate HeaderDoc tags, NSLog() and more ... It also has a flexible styles tab for putting braces on newline or not, for spacing, for custom argument names etc. Most things are handled through Services, so you simply select your ivar block, hit a keystroke or two and you're done. If you minimize Accessorizer to the dock, its interface doesn't come to the front, allowing you to stay focused in Xcode or any other editor that supports Services. Of course, Accessorizer also writes out explicit accessors (as in Objective-C 1.0) and allows you to override properties - all with a simple toggle of a switch. You can even customize the override based on the type passed in. Watch the videos to see it in action.

Kevin Callahan
+1  A: 

This is a python script for Xcode 3.2.4 that generates; interface properties, implementation synthesize, and dealloc's. To install, copy this script, go to Xcode scripts menu (2nd to last) "Edit User Scripts..." Add it under Code, create a new script name, and paste the python script below.

To use just select the variables under the @interface, then call this script. It will then add all of the @property's, in the implementation and all of the @synthesize and dealloc's. It won't add IBOutlet to any of your Labels or Buttons since it doesn't know this, but this is easy to add manually.

Indentation of the script below is critical so don't change it.

#!/usr/bin/python


# Takes a header file with one or more instance variables selected
# and creates properties and synthesize directives for the selected properties.

# Accepts google-style instance variables with a tailing underscore and
# creates an appropriately named property without underscore.

# Xcode script options should be as follows:
# Entire Document
# Home Directory
# Discard Output
# Display in Alert

import os
import re
import subprocess

# AppleScripts for altering contents of files via Xcode
setFileContentsScript = """\
on run argv
set fileAlias to POSIX file (item 1 of argv)
set newDocText to (item 2 of argv)
tell application "Xcode"
set doc to open fileAlias
set text of doc to newDocText
end tell
end run \
"""

getFileContentsScript = """\
on run argv
set fileAlias to POSIX file (item 1 of argv)
tell application "Xcode"
set doc to open fileAlias
set docText to text of doc
end tell
return docText
end run \
"""

# Get variables from Xcode
headerFileText = """%%%{PBXAllText}%%%"""
selectionStartIndex = %%%{PBXSelectionStart}%%%
selectionEndIndex = %%%{PBXSelectionEnd}%%%
selectedText = headerFileText[selectionStartIndex:selectionEndIndex]

headerFilePath = """%%%{PBXFilePath}%%%"""

# Look for an implementation file with .m or .mm extension
implementationFilePath = headerFilePath[:-1] + "m"
if not os.path.exists(implementationFilePath):
implementationFilePath += "m"

instanceVariablesRegex = re.compile(
"""^\s*((?:(?:\\b\w+\\b)\s+)*(?:(?:\\b\\w+\\b)))\\s*""" + # Identifier(s)
"""([*]?)\\s*""" + # An optional asterisk
"""(\\b\\w+?)(_?\\b);""", # The variable name
re.M)

# Now for each instance variable in the selected section
properties = ""
synthesizes = ""
deallocs = ""

for lineMatch in instanceVariablesRegex.findall(selectedText):
    types = " ".join(lineMatch[0].split()) # Clean up consequtive whitespace

    asterisk = lineMatch[1]
    variableName = lineMatch[2]
    trailingUnderscore = lineMatch[3]

    pointerPropertyAttributes = "(nonatomic, retain) " # Attributes if variable is pointer
    if not asterisk:
        pointerPropertyAttributes = "(nonatomic, assign) "

    newProperty = "@property %s%s %s%s;\n" % (pointerPropertyAttributes,
                                       types,
                                       asterisk,
                                       variableName)

    # If there's a trailing underscore, we need to let the synthesize
    # know which backing variable it's using
    newSynthesize = "@synthesize %s%s;\n" % (variableName,
                                     trailingUnderscore and
                                     " = %s_" % variableName)
    # only do the objects
    if asterisk:
        newDealloc = "    [%s%s release];\n" % (variableName,
                    trailingUnderscore and
                                 " = %s_" % variableName)
    properties += newProperty
    synthesizes += newSynthesize
    # only add if it's an object
    if asterisk:
        deallocs += newDealloc


# Check to make sure at least 1 properties was found to generate
if not properties:
    os.sys.stderr.writelines("No properties found to generate")
    exit(-1)

# We want to insert the new properties either immediately after the last
# existing property or at the end of the instance variable section
findLastPropertyRegex = re.compile("^@interface.*?{.*?}.*?\\n" +
                         "(?:.*^\\s*@property.*?\\n)?", re.M | re.S)
headerInsertIndex = findLastPropertyRegex.search(headerFileText).end()

# Add new lines on either side if this is the only property in the file
addedNewLine = "\n"
if re.search("^\s*@property", headerFileText, re.M):
    # Not the only property, don't add
    addedNewLine = ""

newHeaderFileText = "%s%s%s%s" % (headerFileText[:headerInsertIndex],
                      addedNewLine,
                      properties,
                      headerFileText[headerInsertIndex:])

subprocess.call(["osascript",
      "-e",
      setFileContentsScript,
      headerFilePath,
      newHeaderFileText])


if not os.path.exists(implementationFilePath):
    os.sys.stdout.writelines("No implementation file found")
    exit(0)

implementationFileText = subprocess.Popen(
["osascript",
"-e",
getFileContentsScript,
implementationFilePath],
stdout=subprocess.PIPE).communicate()[0]

# We want to insert the synthesizes either immediately after the last existing
# @synthesize or after the @implementation directive
lastSynthesizeRegex = re.compile("^\\s*@implementation.*?\\n" +
                      "(?:.*^\\s*@synthesize.*?\\n)?", re.M | re.S)

implementationInsertIndex = \
lastSynthesizeRegex.search(implementationFileText).end()

# Add new lines on either side if this is the only synthsize in the file
addedNewLine = "\n"
if re.search("^\s*@synthesize", implementationFileText, re.M):
     # Not the only synthesize, don't add
    addedNewLine = ""

newImplementationFileText = "%s%s%s%s" % \
        (implementationFileText[:implementationInsertIndex],
         addedNewLine,
         synthesizes,
         implementationFileText[implementationInsertIndex:])

subprocess.call(["osascript",
       "-e",
       setFileContentsScript,
       implementationFilePath,
       newImplementationFileText])


implementationFileText = subprocess.Popen(
["osascript",
"-e",
getFileContentsScript,
implementationFilePath],
stdout=subprocess.PIPE).communicate()[0]

# We want to insert the deallocs either immediately after the last existing
# [* release] or after the [super dealloc]
lastDeallocRegex = re.compile("^\\s+\[super dealloc\];?\\n" +
                      "(?:.*^\\s+\[\w release\];?\\n)?", re.M | re.S)

deallocInsertIndex = \
lastDeallocRegex.search(implementationFileText).end() 

addedNewDeallocLine = "\n"
if re.search("^\s*\[\w release\];?", implementationFileText, re.M):
# Not the only dealloc, don't add
addedNewDeallocLine = ""


newImplementationFileText = "%s%s%s%s" % \
         (implementationFileText[:deallocInsertIndex],
          addedNewDeallocLine,
          deallocs,
          implementationFileText[deallocInsertIndex:])

subprocess.call(["osascript",
              "-e",
              setFileContentsScript,
              implementationFilePath,
              newImplementationFileText])      

# Switch Xcode back to header file
subprocess.Popen(["osascript",
        "-e",
        getFileContentsScript,
        headerFilePath],
       stdout=subprocess.PIPE).communicate()
Pat