当前位置: 首页 > Linux

Bash小技巧:介绍一个可以批量复制文件到指定目录的shell脚本

时间:2023-04-07 00:31:45 Linux

本文介绍一个可以批量复制文件到指定目录的shell脚本。假设这个shell脚本的名称是cpfiles.sh。在实际开发工作中,可能需要根据目录结构保存一些修改过的文件,以备备份。这些目录结构可能包含多个子目录,或者需要复制多个文件。如果直接复制外部目录,它将包含未更改的文件。如果手动创建每个子目录,然后复制多个文件,会很麻烦。当前脚本希望将要复制的文件的路径保存到配置文件中。然后解析给定的配置文件,自动创建相应的子目录,并将文件复制到指定目录。配置多个文件路径时,可以批量复制文件。同时对当前脚本进行了扩展,可以处理Android系统repostatus命令打印的修改文件信息。执行repostatus查看Android源码仓库变化时,打印的文件信息格式如下:projectframeworks/base/-mservices/core/java/com/android/server/ServiceThread.javaprojectpackages/apps/Music/-msrc/每个项目后,com/android/music/MusicPicker.java后面都会跟着某个代码仓库相对于Android源码根目录的路径。下一行对应repository下的文件路径,是相对于代码仓库根目录的。例如,上述ServiceThread.java文件在Android源码根目录下的完整路径为“frameworks/base/services/core/java/com/android/server/ServiceThread.java”。您可以使用repostatus列出修改的文件信息。将这些文件信息保存到一个文件中,当前脚本可以解析文件信息,将列出的文件批量复制到指定目录。在其他使用git管理代码的项目上,gitlog--name-status打印的修改文件信息类似于repostatus的信息。仅缺少行“projectproject_path”。如果手动添加此行,则可以使用当前脚本复制git仓库下的文件。脚本代码清单cpfiles.sh脚本的具体代码如下。代码中大部分关键代码都做了详细的注释,方便阅读。本文后面还将提供一个参考调用示例,以帮助理解。#!/bin/bash#在Android源码中,执行repostatus命令,可以查看修改后的文件信息。格式为:#projectframeworks/base/#-mservices/core/java/com/android/server/ServiceThread.java#projectpackages/apps/Music/#-msrc/com/android/music/MusicPicker.java#在这个信息中,每个项目段对应一个代码仓库路径。当需要复制多个#仓库的指定代码文件时,手动复制比较麻烦。当前脚本用于处理#这种格式的配置信息,组装完整的文件路径,复制到指定的目标目录。#同时脚本也进行了扩展,提供了完整的文件路径,不限于复制#Android源代码目录下的代码文件,可以复制任意目录下的文件。#下面设置set-e,一旦报错就停止执行。比如复制一个文件,如果有错误就不会被复制。set-eshow_help(){printf"USAGEcpfiles.sh[source_fileinfos[target_dir]]OPTIONS脚本最多可以提供两个参数,这两个参数是可选的。source_fileinfos:指定配置文件名,用于保存源文件路径信息.如果不提供该参数,默认解析的文件名为'copy-files.txt'target_dir:指定将源文件复制到的目录,如果不提供该参数,将复制到'0-后copying'目录。如果目标目录不存在,则自动创建对应的目录。此参数必须是第二个参数。提供此参数时,还应提供第一个参数。注意配置源的引用格式文件路径信息如下:projectbase_top_dir1/-mfile_sub_path1projectfull_file_path2一般来说,每个项目开头的段对应不同目录下的文件,一个项目后面可以跟一个目录目录路径,本节的源文件路径会自动加上这个目录路径。例如,在上面的例子中,要复制的完整文件路径为base_top_dir1/file_sub_path1如果项目后面没有目录路径,则为要复制的这一段文件路径的完整路径。"}#下面的变量指定配置源文件路径信息的文件名解析文件得到要复制的文件路径SRC_FILEINFO="copy-files.txt"#该脚本第一个参数用于指定配置文件的文件名源文件路径信息#如果不提??供第一个参数,则使用默认的配置文件名if[$#-gt0];thenSRC_FILEINFO="$1"fi#下面的变量指定要复制到的源文件目标目录的名称会在当前工作目录下创建一个新目录COPY_TARGET_TOP_DIR="0-aftercopying"#该脚本的第二个参数用于指定源文件复制到的目标目录名称。#如果第二个If[$#-eq2];然后COPY_TARGET_TOP_DIR="$2"fiif[$#-gt2];thenecho"Error:Thisscriptcanonlyprovideatmost2parameters."show_helpexit1fi#查看当前工作目录下是否有指定的配置文件。如果不存在,将返回错误。如果[!-f"${SRC_FILEINFO}"];thenecho"Error:当前目录中不存在要解析的${SRC_FILEINFO}文件。"show_helpexit2fi#从SRC_FILEINFO配置文件中解析出完整的源文件路径信息后,将此信息写入#FULL_FILEPATH对应的文件中。根据FULL_FILEPATH文件中保存的文件路径进行复制.FULL_FILEPATH="full_filepaths.txt"#从给定文件中分析要复制的源文件路径信息。第一个参数指定解析的文件名。#解析得到的完整源文件路径信息将写入到文件中指定的FULL_FILEPATH变量中。#给定的配置文件配置了要复制的源文件路径信息。具体格式如下:#projectbase_top_dir1/#foofile_sub_path1#project#full_file_path2#配置内容可以分多段。每个部分都以项目开头。每个部分可以配置多个源文件路径。#如果项目后面有目录路径,前面会自动加上这一段的文件路径进入这个目录#路径。比如上面的格式,实际要复制的文件路径是base_top_dir1/file_sub_path1。#这时候,这一段的文件路径前面一定要有一个foo占位字符串。具体内容不限,但必须是Yes。#这种格式是为了符合Androidrepostatus命令打印的文件信息。#如果项目后面没有提供目录路径,则表示该段的文件路径为完整的目录路径。如果#给出的文件路径是相对路径,需要保证执行时的工作目录能够寻址到这个相对路径。parse_file_infos(){如果[$#-ne1];thenecho"Usage:${FUNCNAME}filename"return1fi#给定的第一个参数指定要解析的配置文件名。localparsefile="${1}"#下面的变量对应每段开头的项目字符串。localIDENTIFY_PROJECT="project"localfilelinelastcharlocalheaderproject_dirsub_file_pathfull_file_path#如果文件的最后一行没有以换行符'\n'结尾,读取命令在读取最后一行时会返回false#,从而退出在while循环之后,导致最后一行没有被处理,#会少复制一个文件。使用下面的tail命令获取文件的最后一个字符。由于#$()表达式会去掉输出结果末尾的换行符,如果文件的最后一个字符是换行符,经过“$()”展开后会变为空。展开后可以判断结果是否为空#来确认文件是否以换行符结尾。如果它不以换行符结尾,则使用回显命令#将换行符附加到文件末尾。当字符串不为空时,test-n命令返回t后悔。如果测试-n"$(tail"${parsefile}"-c1)";thenecho>>"${parsefile}"fi#/dev/nullisanemptyfile,输出文件内容为空。重定向到#FULL_FILEPATH文件,清空文件内容,避免影响原创内容。cat/dev/null>"${FULL_FILEPATH}"读取文件行;doheader="$(echo${fileline}|awk'{print$1}')"if["${header}"=="${IDENTIFY_PROJECT}"];thenproject_dir="$(echo${fileline}|awk'{print$2}')"#project_dir用作目录路径,最后一个#字符必须是'/'才能拼装成目录路径。如果'/'末尾没有#,则在变量值后添加'/'字符。如果[-n"${project_dir}"];然后lastchar="${project_dir:-1:1}"如果["${lastchar}"!="/"];然后project_dir="${project_dir}/"fifielif[-n"${fileline}"];then#当文件中有空行时,fileline的内容为空字符串。后面汇编#sub_file_path的值会出现异常,所以用上面的-n判断不为空再处理。##如果当前行不以project开头,则对应要复制的文件路径。#当project_dir不为空时,表示当前解析的segment配置了一个目录路径,#那么源文件路径前面会有一个占位符字符串,所以第二列内容用空格隔开#就是文件小路。如果project_dir为空,则此#行是完整的源文件路径。如果[-n"${project_dir}"];然后sub_file_path="$(echo${fileline}|awk'{print$2}')"elsesub_file_path="${fileline}"fifull_file_path="${project_dir}${sub_file_path}"echo"${full_file_path}">>"${FULL_FILEPATH}"fidone<"${parsefile}"#因为有些文件可能会配置多次,下面将生成的内容进行排序,重复行删除。#sort命令的-u选项表示删除重复行。文件名在-o选项后面提供,指定排序后的内容写到哪个文件#。如果没有提供-o选项,则默认写入标准输出,不会直接修改给定的文件本身。这里指定排序后的内容输出到同一个文件并覆盖文件。sort-u"${FULL_FILEPATH}"-o"${FULL_FILEPATH}"}#这个函数解析给定的文件,从中获取要复制的源文件路径,并将#指定的文件复制到目标目录。给出的第一个参数指定要解析的文件名。#所以给定文件的每一行都对应一个要复制的完整文件路径。copy_src_files(){if[$#-ne1];thenecho"Usage:${FUNCNAME}filepaths"return1fi#给定的第一个参数指定保存源文件完整路径信息的文件Namelocalcopyfiles="$1"localsource_file_path#先创建目标目录。必须先创建此目录,然后cp命令才能复制文件。#创建的目录已经存在时,mkdir命令默认会报错。使用-p选项禁用报告错误。mkdir-pv"${COPY_TARGET_TOP_DIR}"同时读取source_file_path;do#cp命令的--parents选项会自动在源文件路径中包含#的目标目录中创建一个子目录。不必先在目标目录中创建每个子目录再复制目录。#cp--parents-v该命令将打印有关创建中间子目录的信息,从而打印出更多信息。不要先添加-v选项。使用-u选项指定只复制较新的文件。cp--parents-u"${source_file_path}""${COPY_TARGET_TOP_DIR}"done<"$copyfiles"}#将Windows系统的dos格式文件转换成unix格式。Dos格式文件的行尾是\r\n,#而unix格式文件的行尾是\n,\r被认为是有效字符。如果不进行转换,则提供#一个dos格式的文件,最后的文件路径会包含\r字符,并将其作为文件名的一部分#。用cp命令复制时,会提示找不到这样的文件。使用file命令查看#dos格式文件时,打印信息中包含字符串“CRLFlineterminators”。查看#给出的文件信息中是否包含该字符串,可以判断文件是否为dos格式。#如果是dos格式的文件,则执行dos2unix命令将其转换为unix格式的文件。if[["$(fileSRC_FILEINFO)"=~"CRLFlineterminators"]];then#dos2unix命令默认直接修改给定的文件本身并将其覆盖为unix格式。dos2unix"${SRC_FILEINFO}"fi#调用parse_file_infos函数解析给定文件的内容,得到要复制的源文件Path.#解析后的源文件路径存放在FULL_FILEPATH变量名指定的文件中。parse_file_infos"${SRC_FILEINFO}"#调用copy_src_files函数,根据源文件路径,将源文件复制到目标目录。copy_src_files"${FULL_FILEPATH}"echo"Allfileshavebeencopyedtothe'${COPY_TARGET_TOP_DIR}'directory."#将给定的文件和生成的路径信息文件移动到目标目录,记录文件的来源。cp-v"${SRC_FILEINFO}""${COPY_TARGET_TOP_DIR}/"mv-v"${FULL_FILEPATH}""${COPY_TARGET_TOP_DIR}/"exit一个参考测试例子为了测试当前的cpfiles.sh脚本,可以首先执行以下命令创建一些目录和文件:$mkdir-ptop/sub1/left1/left2$mkdir-ptop/sub1/right1/$mkdir-ptop/sub2/sub3$touchtop/sub1/left1/left2/left_file.txt$touchtop/sub1/right1/right_file.txt$touchtop/sub2/sub3/sub_file.txt这些命令在当前工作目录下创建一个顶层目录,顶层目录下有一些子目录和文件.总共创建了三个新文本文件。下面将使用cpfiles.sh脚本复制这三个文本文件,并保存在相应的目录结构中。根据上面创建的目录和文件,可以创建一个copy-files.txt文件,在文件中配置如下内容:projecttop/sub1/aleft1/left2/left_file.txtright1/right_file.txtprojecttop/sub2/sub3/sub_file.txt在这个配置信息中,第一个项目部分后面是top/sub1/目录路径。那么该段下的文件路径会自动加上这个目录路径。例如,配置文件left1/left2/left_file.txt的完整复制路径为“top/sub1/left1/left2/left_file.txt”。在第二个项目部分之后提供目录路径。那么这一段下的文件路径就是完整的目录路径。此时需要保证配置的文件可以根据当前工作目录寻址。将cpfiles.sh脚本和copy-files.txt文件放在当前目录下,并为脚本添加可执行权限。具体执行结果如下:$./cpfiles.shmkdir:createddirectory'0-aftercopy'hascopyallfilesto'0-aftercopy'directory.'copy-files.txt'->'0-aftercopy/copy-files.txt''full_filepaths.txt'->'0-aftercopy/full_filepaths.txt'$ls0-aftercopy/copy-files.txtfull_filepaths.txttop$ls0-aftercopy/top/sub1sub2ok可以看到执行cpfiles.sh脚本后,会在当前目录下新建一个“0-aftercopy”目录。然后将要复制的文件按照原来的目录结构复制到“0-复制后”目录下。同时在“0-aftercopy”目录下会生成给定的copy-files.txt文件和生成的full_filepaths.txt。这两个文件记录了要复制的文件路径信息,以便后面进入该目录时,可以查看该目录下的文件列表信息。测试结束后,可以执行以下命令删除创建的测试目录和文件:rm-r0-aftercopying/top/copy-files.txt