Shell 重命名器






4.92/5 (18投票s)
2001年1月13日

196605

2711
Shell Renamer 是一个 Shell 扩展,支持正则表达式搜索和替换重命名以及文件名交换
目的
还记得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
。该类派生自IShellExtInit
和IContextMenu
。早期代码使用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(®t,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(®t,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(®t); } } // 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日 - 更新了文章以修复我遗漏的一些问题