65.9K
CodeProject 正在变化。 阅读更多。
Home

Shell 重命名器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (18投票s)

2001年1月13日

viewsIcon

196605

downloadIcon

2711

Shell Renamer 是一个 Shell 扩展,支持正则表达式搜索和替换重命名以及文件名交换

ShSwap image

目的

还记得DOS时代那个所有文件操作都在控制台完成的美好时光吗?我至今仍清晰地记得那些日子。我厌倦了为了进行批量重命名而打开命令提示符,于是我决定编写一个外壳扩展来为我完成这项工作。这个外壳扩展有两个功能。第一个,也是最重要的,就是它的批量重命名能力。您可以在桌面上选择所有需要重命名的文件,右键单击,然后选择RegExp rename。第二个功能是简单地交换文件名。如果您选中两个文件,您可以交换它们的名称。那么,正则表达式是如何帮助我们重命名的呢?

对于不熟悉的人来说,正则表达式在UNIX领域已经被使用了多年。它们是一种强大的模式匹配工具。但您可能不知道,在正则表达式模式匹配时,您可以保存匹配到的内容。这就是我的外壳扩展的工作原理。您指定一个匹配文件名的正则表达式,它会存储文件名、扩展名等信息,然后您指定一个输出格式来重命名文件。听起来很复杂?是的,某种程度上是这样,但这里有一些例子可以帮助您了解它的用途。

用法

如果选择2个以上的文件,该外壳扩展允许在资源管理器中进行批量重命名。如果恰好选择2个文件,您也可以交换它们的文件名。

模式匹配基于egrep的模式匹配。由于我使用了GNU正则表达式库,您可以编程更改模式匹配的方式。有选项可以模拟sed, awk, grep, egrep等。该外壳扩展可以扩展,允许用户精确指定他想要模拟的程序。

描述正则表达式模式匹配的工作原理超出了本文的范围。我假设您已经知道了 :) 如果您不知道,那么网上有大量的文档,我也可以推荐O'Reilly的《UNIX in a Nutshell》作为入门。

基本上,该外壳扩展的工作方式与emacs的正则表达式搜索和替换非常相似。您匹配您的字符串(在本例中是文件名),然后用包含\[1-9](\1, \2等)的模式进行替换。\[1-9]对应于您匹配到的内容。例如,如果我使用(.*)\.txt作为我的正则表达式,并想用\1.txt替换它,它就会将文件重命名为它找到的任何内容。还有一个选项可以指定正则表达式搜索是否区分大小写。这里有一些示例用法。

要匹配的正则表达式替换格式Effect
(.*)\.txt</td><td><code>prefix\1.txt将*.txt重命名为prefix*.txt
(.*)\.txt</td><td><code>\1suffix.txt将*.txt重命名为*suffix.txt
CSC200.*</td><td><code>CSc200\1将CSC200*重命名为CSc200*

我主要在处理mp3文件时使用此工具。如果我下载了一个现场演出,我不喜欢文件的命名格式,我可以一次性重命名它们。我还用它来转换文件的大小写。另一个用途是将一组.c文件转换为.cpp文件。可能性是无限的!

要使用该工具,首先使用regsvr32注册该扩展。然后,在资源管理器中,选择您要重命名的文件。右键单击,选择RegExp rename或Swap Filenames。使用RegExp rename时,将出现一个对话框,您可以在其中填写正则表达式模式和替换格式。当您更改这些字段时,您将看到更改的结果。这样您就可以在实际重命名文件之前看到它们将被如何重命名。每个文件都可以被选中或取消选中。未选中的文件将不会被重命名。点击OK以重命名所有文件。一旦点击OK,您将看到每个文件重命名的状态。一个列将被添加到列表视图中,显示结果——成功或失败原因。一个新的按钮将出现——Undo(撤销)。如果您不喜欢更改,或者某些结果失败了,您可以撤销。再次点击OK退出。

代码

该项目是一个ATL项目,公开了一个类CISHRename。该类派生自IShellExtInitIContextMenu。早期代码使用STL将文件名存储在vector中,但我已将其更改为简单的C++数组。QueryContextMenu的代码只是创建并用文件名填充此数组。它按名称和目录分隔文件。它需要知道文件的目录,因为正则表达式匹配只作用于文件名,而不作用于完整路径。(例如,d.*\.txt会匹配document.txt,但不会匹配c:\documents\document.txt)
HRESULT CISHRename::Initialize(LPCITEMIDLIST pidlFolder,
                               LPDATAOBJECT pDO,HKEY progID)
{
  HRESULT retVal=S_OK;
  char fileName[MAX_PATH];
  UINT iCount;
  UINT i;
  LPTSTR pEndingSlash;

  if (pDO == NULL)
    return E_INVALIDARG;
  
  // get all the files into our data structures
  STGMEDIUM med;
  FORMATETC fe={CF_HDROP,NULL,DVASPECT_CONTENT,-1,TYMED_HGLOBAL};
  if (SUCCEEDED(pDO->GetData(&fe,&med)))
  {
    // get count so i can resize my array
    iCount=DragQueryFile((HDROP)med.hGlobal,0xFFFFFFFF,NULL,0);
    // we have to have 2+ things
    if (iCount < 2)
      return E_INVALIDARG;

    // resize it
    m_files = new RENAME[iCount];
    if (m_files)
    {
      m_count = iCount;
      // go through all files, add them
      for (i=0;i<iCount;++i)
      {
        if (DragQueryFile((HDROP)med.hGlobal,i,fileName,MAX_PATH) != 0)
        {
          // parse out directory name/file name and store them separately

          // get file name
          pEndingSlash = _tcsrchr(fileName,'\\');
          if (!pEndingSlash)
          {
            m_files[i].dir[0] = 0;
            lstrcpy(m_files[i].file,fileName);
          }
          else
          {
            *pEndingSlash = 0; // break it up

            lstrcpy(m_files[i].dir,fileName);
            lstrcpy(m_files[i].file,pEndingSlash+1);
          }
          
          // rename it by default, but set renamed file to nothing 
          // (will get filled later)
          m_files[i].bRename = true;
          m_files[i].renamedFile[0] = 0;
        }
      }
    }
    else
    {
      m_count = 0;
      retVal = E_OUTOFMEMORY;
    }
    ReleaseStgMedium(&med);    // don't forget to clean up
  }
  return retVal;
  
}

由于它涉及到重命名,所以它分别存储每个文件的文件名和目录。起初我使用 GetFileTitle来提取文件名,但在仔细阅读文档后,如果用户隐藏了已注册文件类型的扩展名,这将不起作用。所以我改回了手动解析。

QueryContextMenu中,我们添加菜单项。如果文件数为2,我们还必须添加Swap Filenames(交换文件名)项。

HRESULT CISHRename::QueryContextMenu(HMENU hmenu, UINT indexMenu,
                                     UINT idCmdFirst, UINT idCmdLast,
                                     UINT uFlags)
{
  UINT idFirst=idCmdFirst;
  UINT iCount;
  if (uFlags & CMF_DEFAULTONLY )
  {
    return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
  }
  else 
  {
    iCount=1;
    // if we have two exactly, make the swap file name option visible
    if (m_count == 2)
    {
      InsertMenu(hmenu,indexMenu++,MF_STRING | MF_BYPOSITION,
                     idFirst+1,(LPCTSTR)_T("Swap filenames"));
      ++iCount;
    }
    
    InsertMenu(hmenu,indexMenu,MF_STRING | MF_BYPOSITION,
                   idFirst,(LPCTSTR)_T("RegExp rename"));
    
    return MAKE_HRESULT(SEVERITY_SUCCESS,FACILITY_NULL,iCount);
  }
  return E_FAIL;
}

重命名代码实际上非常简单,因为正则表达式匹配是由GNU库完成的。对话框过程处理所有细节。当用户更改文本时,对话框会更新与每个文件相关的所有变量(如新的目标名称)。然后当用户点击OK时,它会执行重命名所需的MoveFile操作。以下是处理用户输入的代码。

// Updates the list view based on what the user types in the boxes
static void ChangeListView(HWND hWnd,LPDDATA pData)
{
  char regExp[MAX_PATH];
  char rename[MAX_PATH];
  LVITEM item;
  regex_t regt;
  regmatch_t regMatch[10];
  UINT i;
  unsigned int regFlags;
  HWND hList = GetDlgItem(hWnd,IDC_LIST);

  // reset flags
  for (i=0;i<pData->count;++i)
  {
    pData->pRename[i].bRename = false;
    pData->pRename[i].renamedFile[0] = 0;
  }

  // get the dialog text
  if (GetDlgItemText(hWnd,IDC_EDITREGEXP,regExp,MAX_PATH) != 0 &&
    GetDlgItemText(hWnd,IDC_EDITRENAME,rename,MAX_PATH) != 0)
  {
    // text was good
    // now compile it, and try to run everything through
    re_syntax_options = RE_SYNTAX_EGREP;
    regFlags = REG_EXTENDED;
    if (pData->bCaseInsensitive)
      regFlags |= REG_ICASE;
    // if it compiles
    if (regcomp(&regt,regExp,regFlags) == 0)
    {
      // now check it for each item
      for (i=0;i<pData->count;++i)
      {
        memset(regMatch,-1,sizeof(regMatch));
        // if it matches the file name
        if (regexec(&regt,pData->pRename[i].file,10,regMatch,0) == 0)
        {
          if (RegMoveFile(regMatch,pData->pRename[i].file,rename,
                          pData->pRename[i].renamedFile,MAX_PATH))
            pData->pRename[i].bRename = true;
          else
            pData->pRename[i].renamedFile[0] = 0;
        }
      }
      regfree(&regt);
    }
  }
  // ok now go through each item and fill the list view
  for (i=0;i<pData->count;++i)
  {
    item.mask = LVIF_TEXT;
    item.iItem = i;
    item.iSubItem = 1;
    item.pszText = pData->pRename[i].bRename ? 
                      pData->pRename[i].renamedFile : "<error>";
    SendMessage(hList,LVM_SETITEMTEXT,i,(LPARAM)&item);
  }
}

RegMoveFile函数完成了大部分工作。它使用DFA创建格式化字符串。它展开所有的\1、\2等。

// this function takes the regmatch_t array from regexec and the filename to be
// renamed and the string used for renaming.  It parses the string and picks out
// each \1, \2, \3 etc and replaces it in the string with whatever you specified
// in the regexp match.  
// Example 
bool RegMoveFile(regmatch_t *pMatch,const char *fileName, 
                 const char *outputFormat,char *output, int maxLength)
{
  // a handy macro which won't overwrite the buffer.  Overwrites 
  // are possible because a user can specify a regular expression 
  // and then rename the file as \1\1\1\1\1\1, which could potentially
  // create a gigantic string, overflowing
#define APPENDCHAR(c) if ((--maxLength) > 0) *(s++) = c; else goto exit;

  char *s=output;
  char c;
  int index;
  int length;
  
  // our finite state automata
  enum STATE {S_START,S_SLASH,S_END};

  STATE state=S_START;
  while (state != S_END)
  {
    c=*(outputFormat++);    // chew up next character
    switch (state)
    {
    case S_START:
      if (c == '\0') state=S_END; // null terminated?  we're done
      else if (c == '\\') state=S_SLASH;  // slash? enter slash-found state
      else APPENDCHAR(c);      // anything else, just append it to s
      break;
    case S_SLASH:
      if (c == '\0')    // terminated with \?  just add to s and quit
      {
        APPENDCHAR('\\');
        state=S_END;
      }
      else if (isdigit(c))      // digit?  
      {
        index=c-'0';

        // make sure it's valid
        if (pMatch[index].rm_so < 0 || pMatch[index].rm_eo < 0)
          return false;

        // get length of match
        length=pMatch[index].rm_eo - pMatch[index].rm_so;
        
        // make sure we have enough room for it now
        if (length < maxLength)
        {
          // copy into buffer
          strncpy(s,fileName+pMatch[index].rm_so,length);
          // increment s pointer accordingly
          s += length;
          maxLength -= length;
        }
        else
          goto exit;

        state=S_START;
      }
      else // it's just another character, add to s
      {
        APPENDCHAR(c);
        state=S_START;
      }
      break;
    case S_END: // do nothing
      break;
    }
  }
  // null terminate when we come out
exit:
  *s=0;
  return true;
}

最后的想法

代码可以在Win9x和Windows 2000下编译和运行。我没有测试NT 4.0,但它应该可以工作。它以ANSI模式运行,因为regex库需要ANSI字符串。

在源代码中,我包含了GNU regex库的编译好的静态库副本。regex.h头文件也包含在内。对于那些有兴趣下载源代码的人,可以在ftp.gnu.org找到。非常感谢GNU社区。

修订历史

2002年6月30日 - 将图片移至下载下方

2002年7月27日 - 在我重写后更新了文章

2002年8月7日 - 更新了文章以修复我遗漏的一些问题

© . All rights reserved.