#126938 by sun - add coder_format script to be distributed with coder
authorDoug Green
Sat, 7 Apr 2007 15:58:01 +0000 (15:58 +0000)
committerDoug Green
Sat, 7 Apr 2007 15:58:01 +0000 (15:58 +0000)
coder_format implements an alternative solution to formatting source code

scripts/coder_format/README [new file with mode: 0644]
scripts/coder_format/coder_format.inc [new file with mode: 0644]
scripts/coder_format/coder_format.php [new file with mode: 0644]

diff --git a/scripts/coder_format/README b/scripts/coder_format/README
new file mode 100644 (file)
index 0000000..8aa37ce
--- /dev/null
@@ -0,0 +1,9 @@
+This is a standalone script developed by sun that only concentrates on
+formatting code and the style review.
+
+This is an alternative approach to the coder module.  Having two different
+modules with different approaches is, in some ways an advantage, since each
+becomes - in effect - a unit test upon the other. This is a great way for them
+to both improve, and maintain accuracy in what is a very detail oriented area!
+
+See http://drupal.org/node/126938.
diff --git a/scripts/coder_format/coder_format.inc b/scripts/coder_format/coder_format.inc
new file mode 100644 (file)
index 0000000..5cf3c6e
--- /dev/null
@@ -0,0 +1,1026 @@
+<?php
+// $Id$
+
+/**
+ * Recursively process .module and .inc files in directory.
+ * 
+ * @param bool undo Whether to undo batch replacements.
+ */
+function coder_format_recursive($root, $undo = false, $file_inc = null) {
+  // Convert Windows paths
+  $root = str_replace("\\", '/', $root);
+  
+  // Check if directory exists
+  if (!file_exists($root)) {
+    echo 'ERROR: '. $root .' does not exist!';
+    return;
+  }
+  
+  // Trim root directory
+  if (substr($root, -1) == '/') {
+    $root = substr($root, 0, -1);
+  }
+  
+  // Include Drupal's file.inc
+  if (!isset($file_inc)) {
+    if (!file_exists($root .'/includes/file.inc')) {
+      echo 'ERROR: '. $root .'/includes/file.inc not found!';
+      return;
+    }
+  }
+  else {
+    if (!file_exists($file_inc)) {
+      echo 'ERROR: '. $file_inc .' not found!';
+      return;
+    }
+  }
+  require_once (!isset($file_inc) ? $root .'/includes/file.inc' : $file_inc);
+  
+  if (!$undo) {
+    // Fetch files to process
+    $mask = '.module$|.inc$|.install|.profile$';
+    $nomask = array('.', '..', 'CVS', '.svn');
+    $files = file_scan_directory($root, $mask, $nomask, 0, true);
+    foreach ($files as $file) {
+      // file_copy() assigns $file->filename by reference
+      $sourcefile = $file->filename;
+      echo $file->filename ."\n";
+      file_copy($file->filename, $file->filename .'.coder.orig');
+      coder_format_file($sourcefile, $sourcefile);
+    }
+  }
+  else {
+    // Fetch files to process
+    $mask = '.coder.orig$';
+    $nomask = array('.', '..', 'CVS', '.svn');
+    $files = file_scan_directory($root, $mask, $nomask, 0, true);
+    foreach ($files as $file) {
+      $file->origname = str_replace('.coder.orig', '', $file->filename);
+      echo $file->origname ."\n";
+      file_move($file->filename, $file->origname, FILE_EXISTS_REPLACE);
+    }
+  }
+}
+
+/**
+ * Reads, processes and writes the source code from and to a file.
+ */
+function coder_format_file($sourcefile = null, $targetfile = null) {
+  if (!isset($sourcefile) || !isset($targetfile)) {
+    return;
+  }
+  
+  // Read source code from source file
+  $fd = fopen($sourcefile, 'r');
+  $code = fread($fd, filesize($sourcefile));
+  fclose($fd);
+  
+  if ($code !== false) {
+    // Preprocess source code
+    $code = coder_exec_processors($code, 'coder_preprocessor');
+    
+    // Process source code
+    $code = coder_format_string($code);
+    
+    // Postprocess source code
+    $code = coder_exec_processors($code, 'coder_postprocessor');
+    
+    // Fix beginning and end of code
+    $code = coder_trim_php($code);
+    
+    if ($code !== false) {
+      // Write formatted source code to target file
+      $fd = fopen($targetfile, 'w');
+      $status = fwrite($fd, $code);
+      fclose($fd);
+      return $status;
+    }
+  }
+  
+  return false;
+}
+
+/**
+ * Format the source code according to Drupal coding style guidelines.
+ *
+ * This function uses PHP's tokenizer functions.
+ * @see <http://www.php.net/manual/en/ref.tokenizer.php>
+ *
+ * To achieve the desired coding style, we have to take some special cases
+ * into account. These are:
+ *
+ * Indent-related:
+ *   $_coder_indent int Indent level
+ *      The number of indents for the next line. This is
+ *      - increased after {, : (after case and default).
+ *      - decreased after }, break, case and default (after a previous case).
+ *   $in_case bool
+ *      Is true after case and default. Is false after break and return, if
+ *      $braces_in_case is not greater than 0.
+ *   $braces_in_case int Count of braces
+ *      The number of currently opened curly braces in a case. This is needed
+ *      to support arbitrary function exits inside of a switch control strucure.
+ *   $parenthesis int Parenthesis level
+ *      The number of currently opened parenthesis. This
+ *      - prevents line feeds in brackets (f.e. in arguments of for()).
+ *      - is the base for formatting of multiline arrays. Note: If the last
+ *        ');' is not formatted to the correct indent level then there is no
+ *        ',' (comma) behind the last array value.
+ *   $in_brace bool
+ *      Is true after left curly braces if they are in quotes, an object or
+ *      after a dollar sign. Prevents line breaks around such variable
+ *      statements.
+ *   $in_heredoc bool
+ *      Is true after heredoc output method and false after heredoc delimiter.
+ *      Prevents line breaks in heredocs.
+ *   $first_php_tag bool
+ *      Is false after the first PHP tag. Allows inserting a line break after
+ *      the first one.
+ *   $in_do_while bool
+ *      Is true after a do {} statement and set to false in the next while
+ *      statement. Prevents a line break in the do {...} while() construct.
+ *
+ * Whitespace-related:
+ *   $in_object bool
+ *      Prevents whitespace after ->.
+ *      Is true after ->. Is reset to false after the next string or variable.
+ *   $in_at bool
+ *      Prevents whitespace after @.
+ *      Is true after @. Is reset to false after the next string or variable.
+ *   $in_quote bool
+ *      Prevents
+ *      - removal of whitespace in double quotes.
+ *      - injection of new line feeds after brackets in double quotes.
+ *   $inline_if bool
+ *      Controls formatting of ? and : for inline ifs until a ; (semicolon) is
+ *      processed.
+ *
+ * @param string $code
+ *      The source code to format.
+ *
+ * @return mixed $result
+ *      Returns the formatted code or false if it fails.
+ */
+function coder_format_string($code = '') {
+  global $_coder_indent;
+  
+  // indent controls
+  $_coder_indent  = 0;
+  $in_case        = false;
+  $parenthesis    = 0;
+  $braces_in_case = 0;
+  $in_brace       = false;
+  $in_heredoc     = false;
+  $first_php_tag  = true;
+  $in_do_while    = false;
+  
+  // whitespace controls
+  $in_object   = false;
+  $in_at       = false;
+  $in_php      = false;
+  $in_quote    = false;
+  $inline_if   = false;
+  
+  $result      = '';
+  $lasttoken   = array(0);
+  $tokens      = token_get_all($code);
+
+  // Mask T_ML_COMMENT (PHP4) as T_COMMENT (PHP5).
+  // Mask T_DOC_COMMENT (PHP5) as T_ML_COMMENT (PHP4).
+  if (!defined('T_ML_COMMENT')) {
+    define('T_ML_COMMENT', T_COMMENT);
+  }
+  else if (!defined('T_DOC_COMMENT')) {
+    define('T_DOC_COMMENT', T_ML_COMMENT);
+  }
+  
+  foreach ($tokens as $token) {
+    if (is_string($token)) {
+      // simple 1-character token
+      $text = trim($token);
+      switch ($text) {
+        case '{':
+          // Write curly braces at the end of lines followed by a line break if
+          // not in quotes (""), object ($foo->{$bar}) or in variables (${foo}).
+          // [T_DOLLAR_OPEN_CURLY_BRACES exists but is never assigned.]
+          if (!$in_quote && !$in_object && substr(rtrim($result), -1) != '$') {
+            if ($in_case) {
+              ++$braces_in_case;
+            }
+            ++$_coder_indent;
+            $result = rtrim($result) .' '. $text . coder_br();
+          }
+          else {
+            $in_brace = true;
+            $result .= $text;
+          }
+          break;
+        
+        case '}':
+          if (!$in_quote && !$in_brace && !$in_heredoc) {
+            if ($in_case) {
+              --$braces_in_case;
+            }
+            --$_coder_indent;
+            if ($braces_in_case < 0) {
+              // Decrease indent if last case in a switch is not terminated
+              --$_coder_indent;
+              $in_case = false;
+              $braces_in_case = 0;
+            }
+            $result = rtrim($result);
+            if (substr($result, -1) != '{') {
+              // Avoid line break in empty curly braces
+              $result .= coder_br();
+            }
+            $result .= $text . coder_br();
+          }
+          else {
+            $in_brace = false;
+            $result .= $text;
+          }
+          break;
+        
+        case ';':
+          $result = rtrim($result) . $text;
+          if (!$parenthesis && !$in_heredoc) {
+            $result .= coder_br();
+          }
+          else {
+            $result .= ' ';
+          }
+          if ($inline_if) {
+            $inline_if = false;
+          }
+          break;
+        
+        case '?':
+          $inline_if = true;
+          $result .= ' '. $text .' ';
+          break;
+        
+        case ':':
+          if ($inline_if) {
+            $result .= ' '. $text .' ';
+          }
+          else {
+            if ($in_case) {
+              ++$_coder_indent;
+            }
+            $result = rtrim($result) . $text . coder_br();
+          }
+          break;
+        
+        case '(':
+          $result .= $text;
+          ++$parenthesis;
+          break;
+        
+        case ')':
+          if (!$in_quote && !$in_heredoc && substr(rtrim($result), -1) == ',') {
+            // Fix indent of right parenthesis in multiline arrays by
+            // increasing indent for each parenthesis and decreasing one level.
+            $_coder_indent = $_coder_indent + $parenthesis - 1;
+            $result = rtrim($result) . coder_br() . $text;
+            $_coder_indent = $_coder_indent - $parenthesis + 1;
+          }
+          else {
+            $result .= $text;
+          }
+          if ($parenthesis) {
+            --$parenthesis;
+          }
+          break;
+        
+        case '@':
+          $in_at = true;
+          $result .= $text;
+          break;
+        
+        case ',':
+          $result .= $text .' ';
+          break;
+        
+        case '.':
+          if (substr(rtrim($result), -1) == "'" || substr(rtrim($result), -1) == '"') {
+            // Write string concatenation character directly after strings
+            $result = rtrim($result) . $text .' ';
+          }
+          else {
+            $result = rtrim($result) .' '. $text .' ';
+          }
+          break;
+        
+        case '=':
+        case '<':
+        case '>':
+        case '+':
+        case '*':
+        case '/':
+        case '|':
+        case '^':
+        case '%':
+          $result = rtrim($result) .' '. $text .' ';
+          break;
+        
+        case '&':
+          if (substr(rtrim($result), -1) == '=' || substr(rtrim($result), -1) == '(' || substr(rtrim($result), -1) == ',') {
+            $result .= $text;
+          }
+          else {
+            $result = rtrim($result) .' '. $text .' ';
+          }
+          break;
+
+        case '-':
+          $result = rtrim($result);
+          // Do not add a space before negative numbers or variables
+          if (substr($result, -1) == '>' || substr($result, -1) == '=' || substr($result, -1) == ',' || substr($result, -1) == ':') {
+            $result .= ' '. $text;
+          }
+          else {
+            $result .= ' '. $text .' ';
+          }
+          break;
+        
+        case '"':
+          // toggle quote if the char is not escaped
+          if (rtrim($result) != "\\") {
+            $in_quote = $in_quote ? false : true;
+          }
+          if (substr($result, -3) == ' . ') {
+            // Write string concatenation character directly before strings
+            $result = rtrim($result);
+          }
+          $result .= $text;
+          break;
+        
+        default:
+          $result .= $text;
+          break;
+      }
+    }
+    else {
+      // If we get here, then we have found not a single char, but a token.
+      // See <http://www.php.net/manual/en/tokens.php> for a reference.
+      
+      // Fetch token array
+      list($id, $text) = $token;
+      
+      // Debugging
+      /*
+      if ($lasttoken[0] == T_WHITESPACE) {
+        $result .= token_name($id);
+      }
+      */
+      
+      switch ($id) {
+        case T_ARRAY:
+          // Write array in lowercase
+          $result .= strtolower(trim($text));
+          break;
+        
+        case T_OPEN_TAG:
+        case T_OPEN_TAG_WITH_ECHO:
+          $in_php = true;
+          // Add a line break between two PHP tags
+          if (substr(rtrim($result), -2) == '?>') {
+            $result .= coder_br();
+          }
+          $result .= trim($text);
+          if ($first_php_tag) {
+            $result .= coder_br();
+            $first_php_tag = false;
+          }
+          else {
+            $result .= ' ';
+          }
+          break;
+        
+        case T_CLOSE_TAG:
+          $in_php = false;
+          // Remove preceding line break for inline PHP output in HTML
+          if (substr(rtrim($result), -1) == ';' || substr(rtrim($result), -1) == ':') {
+            $result = rtrim($result) .' ';
+          }
+          $result .= trim($text);
+          break;
+        
+        case T_OBJECT_OPERATOR:
+          $in_object = true;
+          $result .= trim($text);
+          break;
+        
+        case T_CONSTANT_ENCAPSED_STRING:
+          if (substr($result, -2) == '. ') {
+            // Write string concatenation character directly before strings
+            $result = rtrim($result);
+          }
+          // Move on to T_STRING / T_VARIABLE
+        case T_STRING:
+        case T_VARIABLE:
+          if ($in_object || $in_at) {
+            // No space after object operator ($foo->bar) and error suppression (@function())
+            $result = rtrim($result) . trim($text);
+            $in_object = false;
+            $in_at = false;
+          }
+          else {
+            if (!in_array($lasttoken[0], array(T_ARRAY_CAST, T_BOOL_CAST, T_DOUBLE_CAST, T_INT_CAST, T_OBJECT_CAST, T_STRING_CAST, T_UNSET_CAST))) {
+              // Insert a space after right parenthesis, but not after type casts
+              coder_add_space($result);
+            }
+            $result .= trim($text);
+          }
+          break;
+        
+        case T_ENCAPSED_AND_WHITESPACE:
+          $result .= $text;
+          break;
+        
+        case T_WHITESPACE:
+          // Avoid duplicate line feeds outside arrays
+          $c = $parenthesis ? 0 : 1;
+          
+          for ($c, $cc = substr_count($text, chr(10)); $c < $cc; ++$c) {
+            if ($parenthesis) {
+              // Add extra indent for each parenthesis in multiline definitions (f.e. arrays)
+              $_coder_indent = $_coder_indent + $parenthesis;
+              $result = rtrim($result) . coder_br();
+              $_coder_indent = $_coder_indent - $parenthesis;
+            }
+            else {
+              // Discard any whitespace, just insert a line break
+              $result .= coder_br();
+            }
+          }
+          break;
+        
+        case T_IF:
+        case T_FOR:
+        case T_FOREACH:
+        case T_SWITCH:
+        case T_GLOBAL:
+        case T_STATIC:
+        case T_ECHO:
+        case T_PRINT:
+        case T_NEW:
+        case T_REQUIRE:
+        case T_REQUIRE_ONCE:
+        case T_INCLUDE:
+        case T_INCLUDE_ONCE:
+          coder_add_space($result);
+          // Append a space
+          $result .= trim($text) .' ';
+          break;
+        
+        case T_DO:
+          $result .= trim($text);
+          $in_do_while = true;
+          break;
+        
+        case T_WHILE:
+          if ($in_do_while) {
+            // Write while after right parenthesis for do {...} while()
+            $result = rtrim($result) .' ';
+            $in_do_while = false;
+          }
+          // Append a space
+          $result .= trim($text) .' ';
+          break;
+        
+        case T_ELSE:
+        case T_ELSEIF:
+          // Write else and else if to a new line
+          $result = rtrim($result) . coder_br() . trim($text) .' ';
+          break;
+        
+        case T_CASE:
+        case T_DEFAULT:
+          $braces_in_case = 0;
+          $result = rtrim($result);
+          if (!$in_case) {
+            $in_case = true;
+            // Add a line break between cases
+            if (substr($result, -1) != '{') {
+              $result .= coder_br();
+            }
+          }
+          else {
+            // Decrease current indent to align multiple cases
+            --$_coder_indent;
+          }
+          $result .= coder_br() . trim($text) .' ';
+          break;
+        
+        case T_BREAK:
+          // Write break to a new line
+          $result = rtrim($result) . coder_br() . trim($text);
+          if ($in_case && !$braces_in_case) {
+            --$_coder_indent;
+            $in_case = false;
+          }
+          break;
+        
+        case T_RETURN:
+        case T_CONTINUE:
+          coder_add_space($result);
+          $result .= trim($text) .' ';
+          // Decrease indent only if we're not in a control structure inside a case
+          if ($in_case && !$braces_in_case) {
+            --$_coder_indent;
+            $in_case = false;
+          }
+          break;
+        
+        case T_FUNCTION:
+        case T_CLASS:
+          // Write function and class to new lines
+          $result = rtrim($result);
+          if (substr($result, -1) == '}') {
+            $result .= coder_br();
+          }
+          $result .= coder_br() . trim($text) .' ';
+          break;
+        
+        case T_AND_EQUAL:
+        case T_AS:
+        case T_BOOLEAN_AND:
+        case T_BOOLEAN_OR:
+        case T_CONCAT_EQUAL:
+        case T_DIV_EQUAL:
+        case T_DOUBLE_ARROW:
+        case T_IS_EQUAL:
+        case T_IS_NOT_EQUAL:
+        case T_IS_IDENTICAL:
+        case T_IS_NOT_IDENTICAL:
+        case T_IS_GREATER_OR_EQUAL:
+        case T_IS_SMALLER_OR_EQUAL:
+        case T_LOGICAL_AND:
+        case T_LOGICAL_OR:
+        case T_LOGICAL_XOR:
+        case T_MINUS_EQUAL:
+        case T_MOD_EQUAL:
+        case T_MUL_EQUAL:
+        case T_OR_EQUAL:
+        case T_PLUS_EQUAL:
+        case T_SL:
+        case T_SL_EQUAL:
+        case T_SR:
+        case T_SR_EQUAL:
+        case T_XOR_EQUAL:
+          // Surround operators with spaces
+          if (substr($result, -1) != ' ') {
+            // $result must not be trimmed to allow multi-line if-clauses
+            $result .= ' ';
+          }
+          $result .= trim($text) .' ';
+          break;
+        
+        case T_COMMENT:
+        case T_ML_COMMENT:
+        case T_DOC_COMMENT:
+          if (substr($text, 0, 3) == '/**') {
+            // Prepend a new line
+            $result = rtrim($result) . coder_br() . coder_br();
+            
+            // Remove carriage returns
+            $text = str_replace("\r", '', $text);
+            
+            $lines = explode("\n", $text);
+            $params_fixed = false;
+            for ($l = 0; $l < count($lines); ++$l) {
+              $lines[$l] = trim($lines[$l]);
+              
+              // Add a new line between function description and first parameter description.
+              if (!$params_fixed && substr($lines[$l], 0, 8) == '* @param' && $lines[$l - 1] != '*') {
+                $result .= ' *'. coder_br();
+                $params_fixed = true;
+              }
+              else if (!$params_fixed && substr($lines[$l], 0, 8) == '* @param') {
+                // Do nothing if parameter description is properly formatted.
+                $params_fixed = true;
+              }
+              
+              // Add a new line between function params and return.
+              if (substr($lines[$l], 0, 9) == '* @return' && $lines[$l - 1] != '*') {
+                $result .= ' *'. coder_br();
+              }
+              
+              // Add one space indent to get ' *[...]'
+              if ($l > 0) {
+                $result .= ' ';
+              }
+              $result .= $lines[$l];
+              if ($l < count($lines)) {
+                $result .= coder_br();
+              }
+            }
+          }
+          else {
+            $result .= trim($text);
+            if ($parenthesis) {
+              // Add extra indent for each parenthesis in multiline definitions (f.e. arrays)
+              $_coder_indent = $_coder_indent + $parenthesis;
+              $result = rtrim($result) . coder_br();
+              $_coder_indent = $_coder_indent - $parenthesis;
+            }
+            else {
+              // Discard any whitespace, just insert a line break
+              $result .= coder_br();
+            }
+          }
+          break;
+        
+        case T_INLINE_HTML:
+          $result .= $text;
+          break;
+        
+        case T_START_HEREDOC:
+          $result .= trim($text) . coder_br(false);
+          $in_heredoc = true;
+          break;
+        
+        case T_END_HEREDOC:
+          $result .= trim($text) . coder_br(false);
+          $in_heredoc = false;
+          break;
+        
+        default:
+          $result .= trim($text);
+          break;
+      }
+
+      // Store last token
+      $lasttoken = $token;
+    }
+  }
+  return $result;
+}
+
+/**
+ * Generate a line feed including current line indent.
+ *
+ * @param bool $add_indent
+ *      Whether to add current line indent after line feed.
+ *
+ * @return string
+ *      The resulting string.
+ */
+function coder_br($add_indent = true) {
+  global $_coder_indent;
+  
+  $output = "\n";
+  if ($add_indent && $_coder_indent >= 0) {
+    $output .= str_repeat('  ', $_coder_indent);
+  }
+  return $output;
+}
+
+/**
+ * Write a space in certain conditions.
+ * 
+ * A conditional space is needed after a right parenthesis of an if statement
+ * that is not followed by curly braces.
+ * 
+ * @param string $result
+ *      Current result string that will be checked.
+ * 
+ * @return string
+ *      Resulting string with or without an additional space.
+ */
+function coder_add_space(&$result) {
+  if (substr($result, -1) == ')') {
+    $result .= ' ';
+  }
+}
+
+/**
+ * Trim overall code.
+ *
+ * Strips whitespace at the beginning and end of code,
+ * removes the closing PHP tag and appends two empty lines.
+ */
+function coder_trim_php($code) {
+  // Remove surrounding whitespace
+  $code = trim($code);
+  
+  // Insert CVS keyword Id
+  // Search in the very first 1000 chars, insert only one instance.
+  if (strpos(substr($code, 0, 1000), '$Id') === false) {
+    $code = preg_replace('/<\?php\n/', "<?php\n// \$Id\$\n\n", $code, 1);
+  }
+  
+  // Remove closing PHP tag
+  if (substr($code, -2) == '?>') {
+    $code = rtrim($code, '?>');
+  }
+  
+  // Append two empty lines
+  $code .= str_repeat(chr(10), 2);
+  
+  return $code;
+}
+
+/**
+ * Execute special tasks on source code.
+ *
+ * This function works similar to the Drupal hook and forms system. It searches
+ * for all defined functions with the given prefix and performs a preg_replace
+ * on the source code for each of these functions.
+ *
+ * Processor functions are defined with a associative array containing the
+ * following keys with the corresponding values:
+ *   #title
+ *      A human readable text describing what the processor actually does.
+ *   #search
+ *      The regular expression to search for.
+ *   #replace
+ *      The replacement text for each match.
+ *
+ * Optional definitions:
+ *   #debug
+ *      Set this to true to directly output the results of preg_match_all and
+ *      exit script execution after this processor.
+ *
+ * @param string $code
+ *      The source code to process.
+ * @param string $prefix
+ *      Prefix of the functions to execute.
+ *
+ * @return string
+ *      The processed source code.
+ */
+function coder_exec_processors($code, $prefix = '') {
+  if (empty($prefix)) {
+    return;
+  }
+  $tasks = get_defined_functions();
+  $tasks = $tasks['user'];
+  for ($c = 0, $cc = count($tasks); $c < $cc; ++$c) {
+    if (strpos($tasks[$c], $prefix) === false) {
+      unset($tasks[$c]);
+    }
+    else {
+      $tasks[$tasks[$c]] = call_user_func($tasks[$c]);
+      unset($tasks[$c]);
+    }
+  }
+  uasort($tasks, 'coder_order_processors');
+  foreach ($tasks as $func => $task) {
+    if (!isset($task['#search']) || (!isset($task['#replace']) && !isset($task['#replace_callback']))) {
+      continue;
+    }
+    if (isset($task['#debug'])) {
+      // Output regular expression results if debugging is enabled.
+      preg_match_all($task['#search'], $code, $matches, PREG_SET_ORDER);
+      echo "<pre>";
+      var_dump($matches);
+      echo "</pre>\n";
+      // Exit immediately in debugging mode.
+      exit;
+    }
+    if (isset($task['#replace_callback'])) {
+      $code = preg_replace_callback($task['#search'], $task['#replace_callback'], $code);
+    }
+    else {
+      $code = preg_replace($task['#search'], $task['#replace'], $code);
+    }
+  }
+  
+  return $code;
+}
+
+/**
+ * Orders preprocessors by weight.
+ */
+function coder_order_processors($a, $b) {
+  if (isset($a['#weight']) && isset($b['#weight'])) {
+    return $a['#weight'] - $b['#weight'];
+  }
+  else {
+    return isset($a['#weight']) ? false : true;
+  }
+}
+
+/**
+ * @defgroup coder_preprocessor Preprocessors.
+ * @{
+ */
+function coder_preprocessor_line_breaks_win() {
+  return array(
+    '#title' => 'Convert Windows line breaks to Unix format.',
+    '#weight' => 1,
+    '#search' => '/\r\n/',
+    '#replace' => '\n',
+  );
+}
+
+function coder_preprocessor_line_breaks_mac() {
+  return array(
+    '#title' => 'Convert Macintosh line breaks to Unix format.',
+    '#weight' => 2,
+    '#search' => '/\r/',
+    '#replace' => '\n',
+  );
+}
+
+function coder_preprocessor_ml_array_add_comma() {
+  // @bug coder.module:1010
+  return array(
+    '#title' => 'Append a comma to the last value of multiline arrays.',
+    // ^[\040\t]*(?!\*|\/\/)[^\*\/\n]*? matches anything in front of array, but not comments
+    // \sarray\( prevents matching of in_array() and function calls
+    // (\n|(?X>!\);).+?,?\n) matches a line break or the first array item
+    // (.*?[^,;]) matches the rest array items
+    // ,?(\n\s*)\); matches the end of multiline array, optionally including a comma
+    '#search' => '/(^[\040\t]*(?!\*|\/\/)[^\*\/\n]*?\sarray\()(\n|(?>!\);).+?,?\n)(.*?[^,;]),?(\n\s*\);)/ism',
+    '#replace' => '$1$2$3,$4',
+    #'#debug' => true,
+  );
+}
+
+function coder_preprocessor_inline_comment() {
+  return array(
+    '#title' => 'Move inline comments above remarked line.',
+    '#weight' => 2,
+    // [\040\t] matches only a space or tab
+    // (?!case) prevents matching of case statements
+    // \S prevents matching of lines containing only a comment
+    // [^:] prevents matching of URL protocols
+    // [^;\$] prevents matching of CVS keyword Id comment and double slashes
+    //   in quotes (f.e. "W3C//DTD")
+    '#search' => '@^([\040\t]*)(?!case)(\S.+?)[\040\t]*[^:]//\s*([^;\$]+?)$@m',
+    '#replace' => "$1// $3\n$1$2",
+  );
+}
+
+function coder_preprocessor_php() {
+  return array(
+    '#title' => 'Always use &lt;?php ?&gt; to delimit PHP code, not the &lt;? ?&gt; shorthands.',
+    '#search' => '/<\?(\s)/',
+    '#replace' => "<?php$1",
+  );
+}
+
+function coder_preprocessor_switch_duplicate_exit() {
+  return array(
+    '#title' => 'Either exit a switch case with return *or* break.',
+    '#search' => '/(return\s+.+;)\s+break;/',
+    #'#replace' => '$1',
+  );
+}
+
+/**
+ * @} End of "defgroup coder_preprocessor".
+ */
+
+/**
+ * @defgroup coder_postprocessor Postprocessors.
+ * @{
+ */
+function coder_postprocessor_cvs_id() {
+  return array(
+    '#title' => 'If the CVS keyword $Id$ already exists, append a new line after it.',
+    '#search' => '@^(//.*\$Id.*\$)$@m',
+    '#replace' => "$1\n",
+  );
+}
+
+function coder_postprocessor_multiple_vars() {
+  // @todo Prevent matching of multiple lines separated by a blank line 26/03/2007 sun
+  return array(
+    '#title' => 'Align equal signs of multiple variable assignments in the same column.',
+    // \s* matches whitespace including new lines
+    // \$.+? matches variable names
+    // {3,} requires the pattern to match at least 3 times
+    '#search' => '/^(\s*\$.+? = .+?$){3,}/m',
+    '#replace_callback' => 'coder_replace_multiple_vars',
+  );
+}
+
+function coder_replace_multiple_vars($matches) {
+  // Retrieve all variable name = variable value pairs
+  preg_match_all('/^(\s*)(\$.+?) (.?)= (.+?$)/m', $matches[0], $vars, PREG_SET_ORDER);
+  
+  // Determine the longest variable name
+  $maxlength = 0;
+  foreach ($vars as $var) {
+    if (strlen($var[2]) > $maxlength) {
+      $maxlength = strlen($var[2] . $var[3]);
+    }
+  }
+  
+  // Realign variable values at the longest variable names
+  $return = '';
+  $extra_spaces = 0;
+  for ($c = 0, $cc = count($vars); $c < $cc; ++$c) {
+    if ($maxlength <= 20) {
+      $extra_spaces = $maxlength - strlen($vars[$c][2] . $vars[$c][3]);
+    }
+    $return .= $vars[$c][1] . $vars[$c][2];
+    $return .= str_repeat(' ', $extra_spaces) .' '. $vars[$c][3] .'= ';
+    $return .= $vars[$c][4];
+    if ($c < $cc - 1) {
+      // Append a line break, but not to the last variable assignment
+      $return .= "\n";
+    }
+  }
+  
+  return $return;
+}
+
+function coder_postprocessor_array_rearrange() {
+  // @bug common.inc, comment.module
+  // not yet working properly 25/03/2007 sun
+  return array(
+    '#title' => 'Break array elements into separate lines, indented one level.',
+    // ([\040\t]*) matches blanks and tabs
+    // (.*?array\() matches anything and 'array('
+    // ((.+ => .+, ){3,}) matches all array items, except the last one
+    // (.+ => ([^\(\)]+)) matches the last array item, excluding
+    //   arrays or functions (starting with a left parenthesis) [not supported yet]
+    '#search' => '/^([\040\t]*)(.*?array\()((.+ => .+, ){3,})(.+ => ([^\(\)]+))\)/m',
+    '#replace_callback' => 'coder_replace_array_rearrange',
+  );
+}
+
+function coder_replace_array_rearrange($matches) {
+  // Retrieve all array items, except the last one
+  preg_match_all('/(.+? => .+?,) /', $matches[3], $items);
+  
+  // The original line including array(
+  $return = $matches[1] . $matches[2] ."\n";
+  foreach ($items[1] as $item) {
+    // All array items, except the last one, with extra indent
+    $return .= $matches[1] .'  '. $item ."\n";
+  }
+  // Last array item, with extra indent and comma
+  $return .= $matches[1] .'  '. $matches[5] .",\n";
+  // Closing parenthesis (on a new line)
+  $return .= $matches[1] .')';
+  
+  return $return;
+}
+
+function coder_postprocessor_if_curly_braces() {
+  // @todo This is just a starting point for manual replacement at the moment.
+  return array(
+    '#title' => 'Use curly braces even in situations where they are technically optional.',
+    '#search' => '/if \(.+\) [^\{]+;/',
+    #'#replace_callback' => 'coder_replace_if_curly_braces',
+  );
+}
+
+/**
+ * @} End of "defgroup coder_postprocessor".
+ */
+
+/**
+ * @defgroup coder_format_stub_functions
+ * @{
+ */
+if (!function_exists('variable_get')) {
+  function variable_get($name, $default) {
+    global $conf;
+
+    return isset($conf[$name]) ? $conf[$name] : $default;
+  }
+}
+if (!function_exists('variable_set')) {
+  function variable_set($name, $value) {
+    global $conf;
+
+    $conf[$name] = $value;
+  }
+}
+if (!function_exists('t')) {
+  function t($string, $args = 0) {
+    if (!$args) {
+      return $string;
+    }
+    else {
+      return strtr($string, $args);
+    }
+  }
+}
+if (!function_exists('drupal_set_message')) {
+  function drupal_set_message($message = NULL, $type = 'status') {
+    echo $message;
+  }
+}
+if (!function_exists('watchdog')) {
+  function watchdog($type, $message, $severity = 0, $link = null) {
+    return;
+  }
+}
+if (!function_exists('check_plain')) {
+  function check_plain($text) {
+    return htmlspecialchars($text, ENT_QUOTES);
+  }
+}
+
+/**
+ * @} End of "defgroup coder_format_stub_functions".
+ */
+
diff --git a/scripts/coder_format/coder_format.php b/scripts/coder_format/coder_format.php
new file mode 100644 (file)
index 0000000..b81356e
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Coder format shell invocation script.
+ * 
+ * @param string $sourcefile
+ *      The file containing the source code to process, optionally including a
+ *   filepath.
+ * @param string $targetfile
+ *      The file to save the formatted source code to, optionally including a
+ *   filepath.
+ * @param string --batch-replace $path
+ *      A path to process recursively. Each file in the given path will be replaced
+ *   with the formatted source code. Each original file is automatically backed
+ *      up to <filename>.coder.orig.
+ * @param string --undo $path
+ *   A path to search for *.coder.orig files and revert changes. Each formatted
+ *   file (without extension .coder.orig) is replaced with its backup file.
+ * @param string --file-inc $file_inc
+ *   The full path to file.inc in a Drupal installation. Only needed if the
+ *   path provided for --batch-replace or --undo is not the root directory of a
+ *   Drupal installation. Allows to recursively format a CVS checkout folder.
+ * 
+ * @usage
+ *   php coder_format.php <sourcefile> <targetfile>
+ *   php coder_format.php --batch-replace <path> [--file-inc <path_to_file.inc>]
+ *   php coder_format.php --undo <path> [--file-inc <path_to_file.inc>]
+ * 
+ * @example
+ *   php coder_format.php node.module node.module.formatted
+ *   php coder_format.php node.module node.module
+ *   php coder_format.php --batch-replace /home/drupal
+ *   php coder_format.php --undo /home/drupal
+ * 
+ * If either --batch-replace or --undo parameter is given, Coder Format assumes
+ * that the specified path is the root of a Drupal installation. Coder Format
+ * needs includes/file.inc for batch processing. You can optionally specify the
+ * location of file.inc with the --file-inc parameter.
+ * 
+ * Notes:
+ * - Encapsulate file and path arguments with double quotes on Windows.
+ */
+
+require_once realpath(dirname($_SERVER['PHP_SELF'])) .'/coder_format.inc';
+
+if (!empty($_SERVER['argv'])) {
+  // Remove self-reference
+  array_shift($_SERVER['argv']);
+  
+  $undo     = false;
+  $file_inc = null;
+  
+  foreach ($_SERVER['argv'] as $arg) {
+    switch ($arg) {
+      case '--undo':
+        $op   = array_shift($_SERVER['argv']);
+        $root = array_shift($_SERVER['argv']);
+        $undo = true;
+        coder_format_recursive($root, true);
+        break;
+        
+      case '--batch-replace':
+        $op   = array_shift($_SERVER['argv']);
+        $root = array_shift($_SERVER['argv']);
+        break;
+        
+      case '--file-inc':
+        array_shift($_SERVER['argv']);
+        $file_inc = array_shift($_SERVER['argv']);
+        break;
+    }
+  }
+  
+  if (isset($root)) {
+    coder_format_recursive($root, $undo, $file_inc);
+    exit;
+  }
+  
+  // Process a single file
+  $sourcefile = array_shift($_SERVER['argv']);
+  $targetfile = array_shift($_SERVER['argv']);
+  
+  coder_format_file($sourcefile, $targetfile);
+}
+
+