Renaming parts in Eagle CAD by editing the XML directly

eagle-xml-find-and-replace-script

There’s a lot of ways to burn up your time when designing PCBs, but renaming components can be one of the most frustrating. [Joe Pinzone] wrote in with his solution to the problem. Instead of hunting for each part on the schematic to change them one at a time, he makes a list of the substitutions and then uses a script to make all the changes in the XML files. He didn’t publish a post about his work, but you’ll find the source code he wrote embedded after the break.

The straw that finally broke the camel’s back was a project that included about two hundred components which didn’t seem to have a naming order that made any sense with the actual values of the components. The script is written in C++ (for Windows but [Joe] says this should be easily ported to other systems as well). To use it he creates a CSV file with the current component names in the first column. He then goes through and types what he wants for the new name in the second column. This CSV, along with the BRD and SCH files are then given as inputs for the script (through selecting them all and dragging to the script or as CLI arguments) and it automatically makes the changes.

Of course this is only possible because Cadsoft transitioned to using XML files in Eagle 6.

/*
  Title: EagleCAD XML Batch Component Renamer
  Author: Joe Pinzone, Newbury Ohio
  License: Free ("beer"), with acknowledgement of original author
  Date: January, 2013

  Description:
    * Designed to use a CSV to perform batch renaming of EagleCAD schematic/board components
    * Works only on new Eagle XML format schematic and board files

  To generate CSV file with original component name list:
    * Simply drag ONLY your SCH file or BRD onto the EXE. partsList.csv is automatically generated from that file.

  To perform batch renaming:
    * Generate a CSV with original part names in first column, and replacement names in the second
    * Click and drag your SCH, BRD, and CSV file onto the EXE (use multi-select)
    * ^Alternatively, enter in the file paths as arguments from the command prompt

  Notes:
     * If the entry in the second column of the CSV row (replacement name) is blank, that entry will be skipped,
         and the component will retain it's original name
     * To prevent accidental replacement of identical character strings within the XML file, the program
          looks for the specific XML tags that correspond to component names. Edit the list in the
          "Handy things you can modify" section if Eagle changes things, or if you want to play around.
     * You can only load ONE schematic and ONE board file at a time.
     * You can only have 1000 entries in the CSV file. All others are ignored.
        However, you can increase this by setting the value of #define MAX_CSV_ROWS in the source file.
        I didn't want to take up too much memory unnecessarily, and 1000 is plenty for most people.
     * Live long and prosper
*/

#include <cstring>
#include <string>
#include <iostream>
#include <fstream>

using namespace std;

// Handy things you can modify -----------------------------------------------------------------------------------

   /*
       Path and file name to use when generating a name list
   */
   #define MAX_CSV_ROWS 1000 // change this to allow more entries in the CSV find/replace file if necessary.
                             // The program fails nicely, though - it just ignores entries after row 1000.

   char* partsListGenFile = "./partsList.csv";       // the directory and name of the generated parts list CSV file (if no find/replace CSV file is specified)

   /*
     This is the list of XML entries/tags that contain component names after them (from both SCH and BRD Eagle XML files)
     This list is used to make sure that ONLY component names are modified, and coincidental string matches
        elsewhere in the XML aren't accidentally changed. It happened once to me. It wasn't fun. True story.

     Add/remove tags to this list if Eagle changes their standard, or you decide to adapt this for some other purpose.

     The value of 'numTags' should match the number of elements in the 'xmlTags' array below - make sure they match if you change something!
   */
   int numTags = 5;
   string xmlTags[] = { "part name", \
                        "instance part", \
                        "pinref part", \
                        "element name", \
                        "contactref element" \
                      };

   /*
      When generating the CSV list, we only want one copy of each component name.
      The <part name> XML entries in the schematic files, or the <element name> XML entries in the board files
         only contain one entry per part, so we use those as the lookup strings when generating the CSV.
   */
   string schemXMLTag_part = "part name";
   string boardXMLTag_part = "element name";

// (Global) Variable Declarations  -------------------------------------------------------------------------------

	bool schemFound = false;       // used by parseArguments, marks if schematic file was found in the files loaded
	bool boardFound = false;       // used by parseArguments, marks if board file was found in the files loaded
	bool listFound = false;        // used by parseArguments, marks if find/replace csv file was found in the files loaded

	char* path_schem;              // the file path (full or relative) of the schematic file
	char* path_board;              // the file path (full or relative) of the board file
	char* path_list ;              // the file path (full or relative) of the csv find/replace list

	string list[1000][2];          // The storage array for the find/replace list. FIND string is element 0, REPLACE string is element 1
	int listLength = 0;            // number of populated entries in the list array above (set by the loadList function's return value)

// Function Prototypes	-------------------------------------------------------------------------------------------

	int parseArguments(int argc, char* args[]);    // loads the program arguments (file paths of schematic/board/csv files)

	int processFile(char* XMLFilePath);            // assuming that 'list' is populated, goes line-by-line through specified file and replaces component names
	string processLine(string original);           // used by processFile to process an individual line of text
	string processPart(string original);           // used by processLine to change the part name based on the find/replace list

	int loadList(char* listFilePath);              // parses/loads the list file into memory (into the 'list' array)
	void generateList(char* XMLPath, char* outPath, string xmlReferenceTag); // generates a new CSV component name list from the specified SCH or BRD file

// Program --------------------------------------------------------------------------------------------------------

int main(int argc, char* args[]){

    cout << "EagleCAD XML Batch Component Renamer...\n\n";
    if(!parseArguments(argc, args)) { system("pause"); return 0;} // parse program arguments, pause and close on error

    if(listFound == false){ // if no list was specified, assume that we should generate a parts list instead of doing find/replace
         if(schemFound == true){ generateList(path_schem, partsListGenFile, schemXMLTag_part);} // if schematic file is available, generate the list from that
         else if (boardFound == true){ generateList(path_board, partsListGenFile, boardXMLTag_part);} // if schematic file is not available, generate from board file
    }
    else if (listFound == true){ // if the user specified a find/replace list, do the find/replace
         listLength = loadList(path_list);
         if(schemFound == true){ cout << "Changed <" << processFile(path_schem) << "> lines in schematic.\n"; }
         if(boardFound == true){ cout << "Changed <" << processFile(path_board) << "> lines on board.\n"; }
    }
    cout << endl;
    system("pause");
    return 0;
}

int parseArguments(int argc, char* args[]){
    /*
         Load the file paths from the program arguments into memory
    */
    for(int index = 1; index < argc; index++){
            string buffer = args[index];
            if(buffer.find(".sch") != -1){
                if(schemFound == false){
                    path_schem = args[index];
                    schemFound = true;
                }
                 else{
                      cout << "You may only load ONE schematic file.\n\n";
                      return 0;
                 }
            }

            else if(buffer.find(".brd") != -1){
                 if(boardFound == false){
                     path_board = args[index];
                     boardFound = true;
                 }
                 else{
                      cout << "You may only load ONE board file.\n\n";
                      return 0;
                 }
            }

            else if(buffer.find(".csv") != -1){
                 if(listFound == false){
                     path_list = args[index];
                     listFound = true;
                 }
                 else{
                      cout << "You may only load ONE CSV file.\n\n";
                      return 0;
                 }
            }

            else{
                 cout << "Unknown file: " <<  buffer << endl << endl;
                 return 0;
            }
    }
    if(schemFound == false && boardFound == false){
         cout << "You must specify a schematic (SCH) or board (BRD) file.\n\n";
         return 0;
    }

	return 1;
}

int processFile(char* XMLFilePath){

     ifstream source; // pointer to source file
     ofstream dest;   // pointer to destination file
     int modifiedLines = 0;

     string newPath = XMLFilePath;
     newPath += ".new";

     source.open(XMLFilePath); // i.e. file.sch
     dest.open(newPath.c_str()); // i.e. file.sch.new

     string buffer = "";

     while(!source.eof()){
          getline(source, buffer);
          string temp = processLine(buffer);
          dest << temp << endl;
          if(temp != buffer){ modifiedLines++;}

     }

     source.close();
     dest.close();

     return modifiedLines;
}

string processLine(string original){

    /*
      Things to look for in a schematic file
    */
    string beginning = "";
    string end = "";
    string partName = "";

    for(int tagNum = 0; tagNum < numTags; tagNum++){
        if(original.find("<"+xmlTags[tagNum]+"=\"") != -1){     // look for lines that contain the part names
            beginning = "<"+xmlTags[tagNum]+"=\"";              // this is technically always the same, but I want to be explicit
            break; // shouldn't need this if the line contains only one valid XML tag
        }
    }

    // if there is a name to replace, replace it. Otherwise, return the original string
    if(beginning != ""){ // 'beginning' will still be empty ("") if there isn't a valid tag to be processed
        end = original.substr(beginning.length(),original.length()); // remove the beginning, whatever it may be
        partName = end.substr(0,end.find("\""));                  // clear out the trailing XML, leaving just the name
        end = end.substr(end.find("\""),end.length());               // clear the part name from the 'end'
        return beginning + processPart(partName) + end;              // return the new line with replaced part name
    }
    // we only get here if there was nothing to replace in the 'if' block above
    return original;
}

string processPart(string original){
       for(int index = 0; index < listLength; index++){
           if(original == list[index][0] && list[index][1] != ""){ // don't process lines with no replacement
                return list[index][1];
           }
       }
       return original;
}

int loadList(char* listFilePath){

     ifstream source;
     source.open(listFilePath);
     string buffer = "";
     int entryCount = 0;

     while(!source.eof() && entryCount < MAX_CSV_ROWS){
         buffer = "";
         getline(source, buffer);
         list[entryCount][0] = buffer.substr(0,buffer.find(","));
         list[entryCount][1] = buffer.substr(buffer.find(",")+1,buffer.length());
         entryCount++;

     }

     source.close();
     return entryCount;
}

void generateList(char* XMLPath, char* outPath, string xmlReferenceTag){
     cout << "Generating components list from: \n" << XMLPath << endl << endl;
     ifstream source;
     ofstream dest;

     source.open(XMLPath);
     dest.open(outPath);

     string buffer = "";
     string XMLPretext = "<"+xmlReferenceTag+"=\""; // gotta add the extra characters to look for an actual tag
     int elementCount = 0;
     int lineCount = 0;
     while(!source.eof()){
          buffer = "";
          getline(source, buffer);
          lineCount ++;
          if(buffer.find(XMLPretext) != -1){ // look for lines that contain the part names
               buffer = buffer.substr(XMLPretext.length(),buffer.length()); // clear out the leading XML
               buffer = buffer.substr(0,buffer.find("\"")); // clear out the trailing XML, leaving just the name
               dest << buffer << endl;
               elementCount++;
          }

     }
     source.close();
     dest.close();

     return;
}	// generates CSV parts list from SCH or BRD XML files

Comments

  1. Bogdan says:

    Some CAD software allows you to make changes to an instance or all instances.
    It’s one of the drawbacks of eagle that it is unable…

    What really puzzles me? Why does the whole code need to be placed in the post?

  2. Justin says:

    What about Eagle’s built-in ULP, “renumber-sheet.ulp” ? I use this renumber parts in the schematic all the time. I see the value in having the order follow the location on the PCB, and back-anotating the schematic, but it’s easy to locate a reference designator from the tool and find it on the board, and I prefer to have the parts in order through the schematic sheet. Not sure why editing the XML directly is a good idea.

  3. jrog says:

    Not sure why this is better than just using the included ULP “renumber-schematic.ulp”. I prefer to run that command and adjust as I need/want to renumber components. You can even exclude certain reference designators, such as TP$ or whatever you want.

  4. Alex Rossie says:

    sed -e ‘s/old/new/g’ might be helpful here

  5. Jim says:

    Remember kids, libraries to parse XML exist for a reason…

  6. Joe Pinzone says:

    Hey guys – author here.

    First, I won’t pretend to be an expert script writer, so I do accept and appreciate critique. However, I would like to defend and clarify some of the advantages of my possibly convoluted method, as I think your comparisons to other methods aren’t quite fair.

    First, the rename-sheet.ulp is not very flexible or useful for designs that aren’t laid out top/left to bottom/right. My latest design was highly symmetrical with many identical blocks. Had I used the ULP script, I would have ended up with a parts list where identical components serving identical functions in identical but separate blocks would have effectively been assigned randomly indexed identifiers. Editing in a CSV environment was so much more convenient, and trying to massage the ULP was more trouble than it was worth IMHO.

    Second, as Alex and Squirrel have expressed, some ‘sed’ action can perform a similar function. However, the example given by Squirrel is not a fair comparison by a long shot.

    First, it would only work for one component. Let say you rename “R180″>”R18″. Great – it works perfectly (or does it). Then we just expand the function to add another line says that “R18″ becomes “R15″. Oops. Old R180 just became R15, and so did R18. And by the way, the rotation value of EVERY component that WAS 180 degrees is now 15 degrees because rotation data is stored with an “R” prefix with the exact same formatting as component names – but only the XML tag would tell you that.

    Again, I’m all for critique, but please be play fair and look at the measures I’ve taken to make this robust.

    As for the XML parsing libraries – that’s a straight win for Jim. I had no idea they existed, so thank you!

    • junkbox says:

      Well, in that case, I have a few things to (respectfully) ask; Wouldn’t a map be quicker than an array of strings? Also MAX_CSV_ROWS is never used to initialize the array, just during the bounds checking loop. This could cause a problem if someone unsuspectingly increases it. Otherwise, I have to say great job for creating a tool to accomplish your goals!

      • Joe Pinzone says:

        The reason I didn’t use a map is just because I’m an electrical engineer with no little formal programming education, so maps and I are not well acquainted. If you say it would be quicker, I’m sure you’re right – and would love a link to an explanation of the concept to fill that hole in my knowledge base if you would be so kind.

        The fact that MAX_CSV_ROWS does not initialize the array is an error of negligence – thanks for pointing it out! It was a last-minute addition in an effort to put the program controls all in one place, and I forgot to complete the thought.

  7. treymd says:

    What has it come to that something written in C++ is referred to as a script, but something written in HTML5 is referred to as an App?

  8. Joe Pinzone says:

    If the moderator allows such things, here are download links for the executable (rename it .exe) and the source (cpp).

    https://dl.dropbox.com/u/41434020/batchFindReplace.ex
    https://dl.dropbox.com/u/41434020/batchFindReplace.cpp

    Whether the implementation is the most efficient way or not, it works reliably and you’ll never have to look at the code anyway!

    • ursicin says:

      Sorry for reviving this old thread. I am looking for a solution to change wire names, so actually this code would do exactly that. Unfortunately I doesnt open the wire names. For that I’d have to change the source code and recompile that. The only problem; I have no idea about C++ and how to compile that again.
      Is there an exe file availlable who change wire names as well? Or an other solution?

      Thank you

      • Joe Pinzone says:

        This can be adapted to doing a batch rename of wire names (aka “nets”). I’d be curious as to what situation your’e in that would require batch renaming of nets, but I’m happy to help. My email address is in the format ‘firstname.lastname@gmail.com’.

        • ursicin says:

          Hi Joe,

          Thank you very much. I’ll try to explain my situation:
          Some time ago I started building a camper. Among other things I also planed the electrics on that camper myself. First I did that on paper but after a while it got quite complex. I’m a bit of a techie, so that for its own made it even more complex. Then things were added, changed and so on. After some time it then turned out that I have to change quite a lot. Things had to be changed brcause it turned out that it wouldn’t be ideal or even not possible to build like first planed.
          At that point I decided to do it on a CAD based system so future changes would be easier. The solution had to be payable and powerfull enough without getting to complex for that project. I then decided to go for the Eagle hobbyist version.
          I know, there are better software around for doing this kind of shematics, but afordable “and” powerfull is not really availlable. So at the end I decided for Eagle because of the overall performance compared to the price (not only but for a big part).

          Because I had no idea how to use Eagle I just started. So to say, learning by doing. The project grew up to 50 sheets on eagle at the moment. It should not grow any further, it’s nearly finished ;-)
          The nets were named with for example W100/1.5RD, W101/1.5BK, W103/50BK…W300/01, W300/02…W1xx is the unique wire (or cable) name the numbers after the slash are on single wires (/1.5RD) the cable dimension and the color, on cables (/01, /02) the wire number. These numbers are then used to name the wires in the schematic and to get the links to other pages. I think Eagle was not really designed for doing that like this, and probably the eagle cracks are now hitting their heads on the desk while reading that ;-) Anyway, for that project it works quite allright.

          Because I did this like that, in the mean time I had so many changes that I now have the wire names spread all over the schematic without any structure. For example wire W100 is on sheet 25, W101 on sheet 15, …W150 on sheet 1 and so on. Now I’d like to change this, so W100 is on page 1, W101 would be the second wire on page 1, W102 may be the first wire on page 2 and so on.
          The same with the used connectors but these can be found in the Output file of your original programm.

          In the mean time I got in another forum some advice how to change a lot with sed. It can be a bit dangeous to use (IIRC that was the point you wrote the whole programm) but worked for a lot of things right now. But for the one with the wires it gets far to complicated for my sed-understanding.

          Today I tried again with C++ and now I have a working solution based on Dev-C++, so I’m able to compile myself a working exe from your cpp file. That changes nothing to the fact I have no idea about C++ and how to change it. I tried a few things and at the end I found that changing
          string schemXMLTag_part = “part name”; to string schemXMLTag_part = “net name”; could work. But then I get only the net names without the part names…Would be nice if I got the Net and the Part names in the same output, I can live with that, but if you have a better solution I’d apreciate. Or you may see problems I’ll be running into while doing that like this?

          But now I experience another problem: While importing the output.csv file into libre office calc, changing it and exporting it again, something seems to go wrong, probably with the formatting. Can you tell me what setings I should chose for Importing and exporting so it will work? Comma as separation character? Character set? …?

          I also wouldn’t be angry at you if you may have an even better (automated) solution for my original problem so I wouldn’t have to change all W1xx wires by hand ;-)

          If you don’t want to write here any changes to your cpp file, my email would be name at gmx dot ch.

          Thanks a lot

          • Joe Pinzone says:

            The issue that you are having with the CSV file after editing is due to the fact that editors such as OO Calc and Excel sometimes use different newline characters than those which the C++ functions are looking for (i.e., they may use a carriage return (/r) when a line feed (/n) is expected, or the other way around). Whether you understood that last sentence or not, the solution is to open the CSV in a basic text editor (such as notepad), and simply hit save (then close the file). It won’t appear to do anything, but notepad will replace the carriage returns with line feeds when you hit save, and the script should work as expected.

            The way I wrote the script, it is not going to be possible to do both part and net renaming in one run. You will need to compile for both cases and run them as separate programs. Even though it would be possible to modify the program to do both at once, it could be dangerous if you have any parts or components with the same name as a net. While the situation is unlikely, I would personally elect to keep it safe.

            Here a version modified to work for nets (**untested**):
            https://dl.dropboxusercontent.com/u/41434020/batchFindReplaceNets.cpp

            These are the lines that were changed:

            char* partsListGenFile = “./netList.csv”;

            int numTags = 2;
            string xmlTags[] = { “net name”, “signal name” };

            string schemXMLTag_part = “net name”;
            string boardXMLTag_part = “signal name”;

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s