Skip to content

Latest commit

 

History

History
1190 lines (912 loc) · 42.4 KB

File metadata and controls

1190 lines (912 loc) · 42.4 KB

五、文件和目录

在前一章中,我们讨论了许多重要的主题,包括开发和使用 Go 包、Go 数据结构、算法和 GC。然而,到目前为止,我们还没有开发出任何实际的系统实用程序。这一点很快就会改变,因为从这一非常重要的章节开始,我们将开始在 Go 中开发真正的系统实用程序,学习如何使用 Go 来处理文件系统中各种类型的文件和目录。

您应该始终记住,Unix 将所有内容都视为文件,包括符号链接、目录、网络设备、网络套接字、整个硬盘驱动器、打印机和纯文本文件。本章的目的是说明 Go 标准库如何让我们了解路径是否存在,以及如何搜索目录结构以检测我们想要的文件类型。此外,本章将使用 Go 代码作为证据,证明许多处理文件和目录的传统 Unix 命令行实用程序的实现并不困难。

在本章中,您将学习以下主题:

  • Go 软件包将帮助您操作目录和文件
  • 使用flag包轻松处理命令行参数和选项
  • 在 Go 中开发一个版本的which(1)命令行实用程序
  • 在 Go 中开发一个版本的pwd(1)命令行实用程序
  • 删除和重命名文件和目录
  • 轻松遍历目录树
  • 在 Go 中编写一个版本的find(1)实用程序
  • 在另一个位置复制目录结构

有用的Go包

允许您将文件和目录作为实体进行操作的最重要的包是os包,我们将在本章中广泛使用它。如果你将文件视为带有内容的框,则 AUT1 T1 包允许你移动它们,将它们放入废纸篓中,更改他们的名字,访问它们,并决定你想要使用的是哪一个,而将在下一章中介绍的 Type T2 包。允许您在不太担心箱子本身的情况下操纵箱子的内容!

稍后您将看到的flag包允许您定义和处理自己的标志,并操作 Go 程序的命令行参数。

filepath包非常方便,因为它包含filepath.Walk()函数,允许您以简单的方式遍历整个目录结构。

重新访问命令行参数!

正如我们在第 2 章中所看到的,在 Go中编写程序,您无法使用if语句高效地处理多个命令行参数和选项。这个问题的解决方案是使用flag包,下面将对此进行解释。

记住flag包是一个标准的 Go 包,您不必在其他地方搜索标志的功能,这一点非常重要。

旗包

flag包为我们完成了解析命令行参数和选项的肮脏工作;因此,没有必要编写复杂而复杂的 Go 代码。此外,它还支持各种类型的参数,包括字符串、整数和布尔值,这节省了您的时间,因为您不必执行任何数据类型转换。

usingFlag.go程序说明了flagGo 包的使用,将分三部分介绍。第一部分具有以下 Go 代码:

package main 

import ( 
   "flag" 
   "fmt" 
) 

第二部分是程序中最重要的 Go 代码,如下所示:

func main() { 
   minusO := flag.Bool("o", false, "o") 
   minusC := flag.Bool("c", false, "c") 
   minusK := flag.Int("k", 0, "an int") 

   flag.Parse() 

在本部分中,您可以看到如何定义您感兴趣的标志。在这里,您定义了-o-c-k。尽管前两个是布尔标志,-k标志需要一个整数值,可以用-k=123表示。

最后一部分包含以下 Go 代码:

   fmt.Println("-o:", *minusO) 
   fmt.Println("-c:", *minusC) 
   fmt.Println("-K:", *minusK) 

   for index, val := range flag.Args() { 
         fmt.Println(index, ":", val) 
   } 
} 

在本部分中,您可以看到如何读取选项的值,这也允许您判断选项是否已设置。此外,flag.Args()允许您访问程序中未使用的命令行参数。

usingFlag.go的使用和输出如以下输出所示:

$ go run usingFlag.go
-o: false
-c: false
-K: 0
$ go run usingFlag.go -o a b
-o: true
-c: false
-K: 0
0 : a
1 : b

但是,如果您忘记键入命令行选项的值(-k或提供的值的类型错误,您将收到以下消息,程序将终止:

$ ./usingFlag -k
flag needs an argument: -k
Usage of ./usingFlag:
  -c  c
  -k int
      an int
  -o  o $ ./usingFlag -k=abc invalid value "abc" for flag -k: strconv.ParseInt: parsing "abc": invalid syntax
Usage of ./usingFlag:
  -c  c
  -k int
      an int
  -o  o

如果您不希望程序在出现解析错误时退出,可以使用flag包提供的ErrorHandling类型,它允许您在NewFlagSet()函数的帮助下更改flag.Parse()在错误时的行为方式。但是,在系统编程中,当一个或多个命令行选项中出现错误时,通常希望实用程序退出。

处理目录

目录允许您创建结构,并以便于组织和搜索的方式存储文件。实际上,目录是文件系统中包含其他文件和目录列表的条目。这是在索引节点的帮助下实现的,索引节点是保存文件和目录信息的数据结构。

如下图所示,目录实现为分配给 inode 的名称列表。因此,目录包含其自身、其父目录及其每个子目录的条目,其中包括常规文件或其他目录:

您应该记住,inode 保存的是关于文件的元数据,而不是文件的实际数据。

inode 的图形表示法

关于符号链接

符号链接是指向文件或目录的指针,在访问时解析。符号链接,也称为软链接,不等于它们所指向的文件或目录,并且不允许指向任何地方,这有时会使事情复杂化。

以下 Go 代码保存在symbLink.go中,分两部分显示,允许您检查路径或文件是否为符号链接。第一部分内容如下:

package main 

import ( 
   "fmt" 
   "os" 
   "path/filepath" 
) 

func main() { 
   arguments := os.Args 
   if len(arguments) == 1 { 
         fmt.Println("Please provide an argument!") 
         os.Exit(1) 
   } 
   filename := arguments[1] 

这里没有发生什么特别的事情:您只需要确保您得到一个命令行参数,以便进行测试。第二部分是以下 Go 代码:

   fileinfo, err := os.Lstat(fil /etcename) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 

   if fileinfo.Mode()&os.ModeSymlink != 0 { 
         fmt.Println(filename, "is a symbolic link") 
         realpath, err := filepath.EvalSymlinks(filename) 
         if err == nil { 
               fmt.Println("Path:", realpath) 
         } 
   } 

}

前面提到的symbLink.go代码比通常更神秘,因为它使用较低级别的函数。确定路径是否为真实路径的技术包括使用os.Lstat()函数提供有关文件或目录的信息,以及对os.Lstat()调用的返回值使用Mode()函数,以便将结果与os.ModeSymlink常量(符号链接位)进行比较。

此外,还存在filepath.EvalSymlinks()函数,允许您评估任何存在的符号链接,并返回文件或目录的真实路径,该路径也在symbLink.go中使用。这可能会让你认为我们正在使用大量的 GO 代码来完成这样一个简单的任务,这是部分正确的,但是当你开发系统软件时,你必须考虑所有的可能性,并且要谨慎。

执行只接受一个命令行参数的symbLink.go,生成以下输出:

$ go run symbLink.go /etc
/etc is a symbolic link
Path: /private/etc

在本章的其余部分,您还将看到前面提到的一些 Go 代码作为更大程序的一部分。

执行 pwd(1)命令

当我开始思考如何实施一个计划时,我脑海中浮现出太多的想法,以至于有时很难决定要做什么!这里的关键是做点什么,而不是等待,因为在编写代码时,您将能够判断所采用的方法是否正确,以及是否应该尝试另一种方法。

pwd(1)命令行实用程序非常简单,但它做得非常好。如果你写了很多 shell 脚本,你应该已经知道了pwd(1),因为当你想要获得一个文件的完整路径或者一个与正在执行的脚本位于同一目录中的目录时,它非常方便。

pwd.go的 Go 代码将分为两部分,仅支持-P命令行选项,该选项解析所有符号链接并打印物理当前工作目录。pwd.go的第一部分如下:

package main 

import ( 
   "fmt" 
   "os" 
   "path/filepath" 
) 

func main() { 
   arguments := os.Args 

   pwd, err := os.Getwd() 
   if err == nil { 
         fmt.Println(pwd) 
   } else { 
         fmt.Println("Error:", err) 
   } 

第二部分内容如下:

   if len(arguments) == 1 { 
         return 
   } 

   if arguments[1] != "-P" { 
         return 
   } 

   fileinfo, err := os.Lstat(pwd) 
   if fileinfo.Mode()&os.ModeSymlink != 0 { 
         realpath, err := filepath.EvalSymlinks(pwd) 
         if err == nil { 
               fmt.Println(realpath) 
         } 
   } 
} 

请注意,如果当前目录可以由多个路径描述(如果使用符号链接,可能会发生这种情况),os.Getwd()可以返回其中任何一个路径。此外,如果给出了-P选项,并且您正在处理一个作为符号链接的目录,您需要重用symbLink.go中的一些 Go 代码来发现物理当前工作目录。另外,在pwd.go中不使用 flag 包的原因是我发现代码的方式要简单得多。

执行pwd.go将生成以下输出:

$ go run pwd.go
/Users/mtsouk/Desktop/goBook/ch/ch5/code

在 macOS 机器上,/tmp目录是一个符号链接,可以帮助我们验证pwd.go是否按预期工作:

$ go run pwd.go
/tmp
$ go run pwd.go -P
/tmp
/private/tmp

开发 Go 中的 which(1)实用程序

which(1)实用程序搜索PATH环境变量的值,以确定是否可以在PATH变量的一个目录中找到可执行文件。以下输出显示了which(1)实用程序的工作方式:

$ echo $PATH
/home/mtsouk/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
$ which ls
/home/mtsouk/bin/ls
code$ which -a ls
/home/mtsouk/bin/ls
/bin/ls

我们对 Unix 实用程序的实现将支持 macOS 版本which(1)支持的两个命令行选项,即-a-s,借助flag包:which(1)的 Linux 版本不支持-s选项。-a选项列出可执行文件的所有实例,而不仅仅是第一个实例;-s如果找到可执行文件,则返回0,否则返回1:这与使用fmt包打印01不同。

要检查 shell 中 Unix 命令行实用程序的返回值,应执行以下操作:

$ which -s ls $ echo $?
0

注意,go run打印出非零退出代码。

which(1)的 Go 代码将保存在which.go中,并分四个部分呈现。which.go的第一部分有以下 Go 代码:

package main 

import ( 
   "flag" 
   "fmt" 
   "os" 
   "strings" 
) 

需要使用strings包,以便在读取PATH变量后拆分其内容。which.go的第二部分涉及flag套餐的使用:

func main() { 
   minusA := flag.Bool("a", false, "a") 
   minusS := flag.Bool("s", false, "s") 

   flag.Parse() 
   flags := flag.Args() 
   if len(flags) == 0 { 
         fmt.Println("Please provide an argument!") 
         os.Exit(1) 
   } 
   file := flags[0] 
   fountIt := false 

which.go的一个非常重要的部分是读取PATHshell 环境变量以拆分并使用它的部分,这在第三部分中介绍:

   path := os.Getenv("PATH") 
   pathSlice := strings.Split(path, ":") 
   for _, directory := range pathSlice { 
         fullPath := directory + "/" + file 

这里的最后一条语句构造了我们正在搜索的文件的完整路径,就好像它存在于PATH变量的每个单独目录中一样,因为如果你有一个文件的完整路径,你就不必搜索它!

which.go的最后一部分如下:

         fileInfo, err := os.Stat(fullPath) 
         if err == nil { 
               mode := fileInfo.Mode() 
               if mode.IsRegular() { 
                     if mode&0111 != 0 { 
                           fountIt = true 
                           if *minusS == true { 
                                 os.Exit(0) 
                           } 
                           if *minusA == true {

                                 fmt.Println(fullPath) 
                           } else { 
                                 fmt.Println(fullPath) 
                                 os.Exit(0) 
                           } 
                     } 
               } 
         } 
   } 
   if fountIt == false { 
         os.Exit(1) 
   } 
} 

在这里,对os.Stat()的调用告诉我们正在查找的文件是否确实存在。如果成功,mode.IsRegular()函数将检查该文件是否为常规文件,因为我们没有寻找目录或符号链接。然而,我们还没有完成!which.go程序执行测试,以确定找到的文件是否确实是可执行文件:如果不是可执行文件,则不会打印。因此,if mode&0111 != 0语句使用二进制操作验证该文件实际上是一个可执行文件。

接下来,如果将-s标志设置为*minusS == true,那么-a标志实际上并不重要,因为程序将在找到匹配项后立即终止。

正如您所看到的,which.go中涉及很多测试,这对于系统软件来说并不少见。然而,你应该经常检查所有的可能性,以避免以后发生意外。好的方面是,这些测试中的大多数将在find(1)实用程序的 Go 实现中稍后使用:在将它们全部放在更大的程序中之前,通过编写小程序来测试一些功能是一种很好的做法,因为这样做,您可以更好地学习该技术,并且可以更容易地检测愚蠢的错误。

执行which.go将产生以下输出:

$ go run which.go ls
/home/mtsouk/bin/ls
$ go run which.go -s ls
$ echo $?
0
$ go run which.go -s ls123123
exit status 1
$ echo $?
1
$ go run which.go -a ls
/home/mtsouk/bin/ls
/bin/ls

打印文件或目录的权限位

ls(1)命令的帮助下,您可以找到一个文件的权限:

$ ls -l /bin/ls
-rwxr-xr-x  1 root  wheel  38624 Mar 23 01:57 /bin/ls

在本小节中,我们将了解如何使用 Go 打印文件或目录的权限:Go 代码将保存在permissions.go中,并将分两部分呈现。第一部分内容如下:

package main 

import ( 
   "fmt" 
   "os" 
) 

func main() { 
   arguments := os.Args 
   if len(arguments) == 1 { 
         fmt.Println("Please provide an argument!") 
         os.Exit(1) 
   } 

   file := arguments[1] 

第二部分包含重要的 Go 代码:

   info, err := os.Stat(file) 
   if err != nil { 
         fmt.Println("Error:", err) 
         os.Exit(1) 
   } 
   mode := info.Mode() 
   fmt.Print(file, ": ", mode, "\n") 
} 

同样,大多数 Go 代码都用于处理命令行参数,并确保您有一个!执行实际作业的 Go 代码主要是对os.Stat()函数的调用,该函数返回一个FileInfo结构,描述os.Stat()检查的文件或目录。在FileInfo结构中,您可以通过调用Mode()函数来发现文件的权限。

执行permissions.go产生以下输出:

$ go run permissions.go /bin/ls
/bin/ls: -rwxr-xr-x
$ go run permissions.go /usr
/usr: drwxr-xr-x
$ go run permissions.go /us
Error: stat /us: no such file or directory
exit status 1

处理Go中的文件

操作系统的一项极其重要的任务是处理文件,因为所有数据都存储在文件中。在本节中,我们将向您展示如何删除和重命名文件,在下一节中,在 Go中开发 find(1),我们将教您如何搜索目录结构以找到所需的文件。

删除文件

在本节中,我们将演示如何使用os.Remove()Go 函数删除文件和目录。

当测试删除文件和目录的程序时,要格外小心并使用常识!

rm.go文件是rm(1)工具的 Go 实现,说明了如何在 Go 中删除文件。尽管rm(1)的核心功能已经存在,但rm(1)的选项仍然缺失:尝试实现其中一些功能将是一个很好的练习。在实施-f-R选项时,请特别注意。

rm.go的 Go 代码如下:

package main 
import ( 
   "fmt" 
   "os" 
) 

func main() { 
   arguments := os.Args 
   if len(arguments) == 1 { 
         fmt.Println("Please provide an argument!") 
         os.Exit(1) 
   } 

   file := arguments[1] 
   err := os.Remove(file) 
   if err != nil { 
         fmt.Println(err) 
         return 
   } 
} 

如果rm.go执行时没有任何问题,根据 Unix 原理,它将不会创建任何输出。因此,这里有趣的是,当您试图删除的文件不存在时,您可以看到错误消息:当您没有删除该文件的必要权限时,以及当目录不为空时:

$ go run rm.go 123
remove 123: no such file or directory
$ ls -l /tmp/AlTest1.err
-rw-r--r--  1 root  wheel  1278 Apr 17 20:13 /tmp/AlTest1.err
$ go run rm.go /tmp/AlTest1.err
remove /tmp/AlTest1.err: permission denied
$ go run rm.go test
remove test: directory not empty

重命名和移动文件

在本小节中,我们将向您展示如何使用 Go 代码重命名和移动文件:Go 代码将保存为rename.go。虽然相同的代码可以用于重命名或移动目录,rename.go只允许处理文件。

在执行无法轻松撤消的操作(例如覆盖文件)时,您应该格外小心,可能会通知用户目标文件已经存在,以避免令人不快的意外。虽然传统的mv(1)实用程序的默认操作会自动覆盖目标文件(如果存在),但我认为这不是很安全。因此,rename.go默认情况下不会覆盖目标文件。

在开发系统软件时,你必须处理所有的细节,否则细节会在最不经意的时候暴露为 bug!广泛的测试将允许您找到遗漏的细节并进行纠正。

rename.go的代码将分为四个部分。第一部分包括预期的序言以及处理flag包设置的 Go 代码:

package main 

import ( 
   "flag" 
   "fmt" 
   "os" 
   "path/filepath" 
) 

func main() { 
   minusOverwrite := flag.Bool("overwrite", false, "overwrite") 

   flag.Parse() 
   flags := flag.Args() 

   if len(flags) < 2 { 
         fmt.Println("Please provide two arguments!") 
         os.Exit(1) 
   } 

第二部分具有以下 Go 代码:

   source := flags[0] 
   destination := flags[1] 
   fileInfo, err := os.Stat(source) 
   if err == nil { 
         mode := fileInfo.Mode() 
         if mode.IsRegular() == false { 
               fmt.Println("Sorry, we only support regular files as source!") 
               os.Exit(1) 
         } 
   } else { 
         fmt.Println("Error reading:", source) 
         os.Exit(1) 
   } 

此部分确保源文件存在,是常规文件,而不是目录或其他类似于网络套接字或管道的文件。这里再次使用您在which.go中看到的os.Stat()技巧。

rename.go的第三部分如下:

   newDestination := destination 
   destInfo, err := os.Stat(destination) 
   if err == nil { 
         mode := destInfo.Mode() 
         if mode.IsDir() { 
               justTheName := filepath.Base(source) 
               newDestination = destination + "/" + justTheName 
         } 
   } 

这里还有一个棘手的问题;您需要考虑源是普通文件的情况,并且目的地是一个目录,该目录是借助于 AutoT0-变量来实现的。

另一个特殊的情况,你应该考虑的是,当源文件的格式包含绝对或相对路径,如在这种情况下,当目的地是目录时,您应该获取路径的基本名称,即最后一个/字符后面的路径,在本例中是aFile,并将其添加到目的地目录中,以便正确构造newDestination变量。这是在filepath.Base()函数的帮助下实现的,该函数返回路径的最后一个元素。

最后,rename.go的最后一部分有如下 Go 代码:

   destination = newDestination 
   destInfo, err = os.Stat(destination) 
   if err == nil { 
         if *minusOverwrite == false { 
               fmt.Println("Destination file already exists!") 
               os.Exit(1) 
         } 
   } 

   err = os.Rename(source, destination) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 
} 

rename.go最重要的 Go 代码与识别目标文件是否存在有关。这也是在os.Stat()函数的支持下实现的。如果os.Stat()返回错误消息,表示目标文件不存在;因此,您可以随时拨打os.Rename()。如果os.Stat()返回nil,则表示os.Stat()调用成功,且目标文件存在。在这种情况下,您应该检查overwrite标志的值,看看是否允许覆盖目标文件。

当一切正常时,您可以随时拨打os.Rename()并执行所需任务!

如果rename.go执行正确,则不会产生输出。但是,如果有问题,rename.go会产生一些输出:

$ touch newFILE
$ ./rename newFILE regExpFind.go
Destination file already exists!
$ ./rename -overwrite newFILE regExpFind.go
$

在Go中开发 find(1)

本节将向您介绍在 Go 中开发简化版的find(1)命令行实用程序所需了解的必要知识。开发的版本将不支持find(1)支持的所有命令行选项,但它将有足够的选项真正有用。

您将在以下小节中看到的是整个过程的小步骤。因此,第一小节将向您展示访问给定目录树中所有文件和目录的方法。

遍历目录树

find(1)需要支持的最重要的任务是能够访问从给定目录开始的所有文件和子目录。因此,本节将在 Go 中实现此任务。traverse.go的 Go 代码将分为三部分。第一部分是预期的序言:

package main 

import ( 
   "fmt" 
   "os" 
   "path/filepath" 
) 

第二部分是关于实现名为walkFunction()的函数,该函数将用作名为filepath.Walk()的 Go 函数的参数:

func walkFunction(path string, info os.FileInfo, err error) error { 
   _, err = os.Stat(path) 
   if err != nil { 
         return err 
   } 

   fmt.Println(path) 
   return nil 
} 

再次使用os.Stat()函数,因为成功的os.Stat()函数调用意味着我们正在处理实际存在的东西(文件、目录、管道等等)!

不要忘记,在调用filepath.Walk()和调用并执行walkFunction()之间,在一个活动繁忙的文件系统中可能会发生很多事情,这是调用os.Stat()的主要原因。

守则的最后部分如下:

func main() { 
   arguments := os.Args 
   if len(arguments) == 1 { 
         fmt.Println("Not enough arguments!") 
         os.Exit(1) 
   } 

   Path := arguments[1] 
   err := filepath.Walk(Path, walkFunction) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 
} 

在前面定义的walkFunction()函数的帮助下,filepath.Walk()函数会自动完成此处的所有脏作业。filepath.Walk()函数有两个参数:目录的路径和它将使用的 walk 函数。

执行traverse.go将生成以下类型的输出:

$ go run traverse.go ~/code/C/cUNL
/home/mtsouk/code/C/cUNL
/home/mtsouk/code/C/cUNL/gpp
/home/mtsouk/code/C/cUNL/gpp.c
/home/mtsouk/code/C/cUNL/sizeofint
/home/mtsouk/code/C/cUNL/sizeofint.c
/home/mtsouk/code/C/cUNL/speed
/home/mtsouk/code/C/cUNL/speed.c
/home/mtsouk/code/C/cUNL/swap
/home/mtsouk/code/C/cUNL/swap.c

正如您所看到的,traverse.go的代码非常简单,除其他外,它无法区分目录、文件和符号链接。然而,访问给定目录树下的每个文件和目录,这是find(1)实用程序的基本功能,这项工作相当繁琐。

只访问目录!

尽管能够访问所有内容很好,但有时您只想访问目录而不想访问文件。因此,在本小节中,我们将修改traverse.go,以便仍然访问所有内容,但只打印目录名。新程序的名称将为traverseDir.gotraverse.go中唯一需要更改的部分是walkFunction()的定义:

func walkFunction(path string, info os.FileInfo, err error) error { 
   fileInfo, err := os.Stat(path) 
   if err != nil { 
         return err 
   } 

   mode := fileInfo.Mode() 
   if mode.IsDir() { 
         fmt.Println(path) 
   } 
   return nil 
} 

如您所见,这里您需要使用os.Stat()函数调用返回的信息来检查您是否正在处理目录。如果你有一个目录,那么你打印它的路径,你就完成了。

执行traverseDir.go将生成以下输出:

$ go run traverseDir.go ~/code
/home/mtsouk/code
/home/mtsouk/code/C
/home/mtsouk/code/C/cUNL
/home/mtsouk/code/C/example
/home/mtsouk/code/C/sysProg
/home/mtsouk/code/C/system
/home/mtsouk/code/Haskell
/home/mtsouk/code/aLink
/home/mtsouk/code/perl
/home/mtsouk/code/python  

find(1)的第一个版本

本节中的 Go 代码保存为find.go,将分三部分介绍。正如您将看到的,find.go使用了traverse.go中的大量代码,这是逐步开发程序的主要好处。

find.go的第一部分是预期的序言:

package main 

import ( 
   "flag" 
   "fmt" 
   "os" 
   "path/filepath" 
) 

我们已经知道,我们将在不久的将来改进find.go,这里使用flag包,即使这是find.go的第一个版本,并且它没有任何标志!

Go 代码的第二部分包含walkFunction()的实现:

func walkFunction(path string, info os.FileInfo, err error) error { 

   fileInfo, err := os.Stat(path) 
   if err != nil { 
         return err 
   } 

   mode := fileInfo.Mode() 
   if mode.IsDir() || mode.IsRegular() { 
         fmt.Println(path) 
   } 
   return nil 
} 

walkFunction()的实现中,您很容易理解find.go只打印常规文件和目录,而不打印其他内容。这是个问题吗?不,如果这是你想要的。一般来说,这是不好的。尽管如此,尽管有一些限制,但拥有第一个版本的东西仍然可以工作,这是一个很好的起点!下一个版本将命名为improvedFind.go,它将通过添加各种命令行选项来改进find.go

find.go的最后一部分包含实现main()功能的代码:

func main() { 
   flag.Parse() 
   flags := flag.Args() 

   if len(flags) == 0 { 
         fmt.Println("Not enough arguments!") 
         os.Exit(1) 
   } 

   Path := flags[0]

   err := filepath.Walk(Path, walkFunction) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 
} 

执行find.go将创建以下输出:

$ go run find.go ~/code/C/cUNL
/home/mtsouk/code/C/cUNL
/home/mtsouk/code/C/cUNL/gpp
/home/mtsouk/code/C/cUNL/gpp.c
/home/mtsouk/code/C/cUNL/sizeofint
/home/mtsouk/code/C/cUNL/sizeofint.c
/home/mtsouk/code/C/cUNL/speed
/home/mtsouk/code/C/cUNL/speed.c
/home/mtsouk/code/C/cUNL/swap
/home/mtsouk/code/C/cUNL/swap.c

添加一些命令行选项

本小节将尝试改进您先前创建的find(1)的 Go 版本。请记住,这是用于开发实际程序的过程,因为您并没有在程序的第一个版本中实现所有可能的命令行选项。

新版本的 Go 代码将保存为improvedFind.go。除此之外,新版本将能够忽略符号链接:符号链接仅在improvedFind.go与相应的命令行选项一起使用时才会打印。为此,我们将使用symbLink.go的一些 Go 代码。

improvedFind.go程序是一个真正的系统工具,您可以在自己的 Unix 机器上使用。

支持的标志如下所示:

  • -s:用于打印套接字文件
  • -p:用于打印管道
  • -sl:用于打印符号链接
  • -d:用于打印目录
  • -f:用于打印文件

正如您将看到的,大多数新的 Go 代码用于支持添加到程序中的标志。此外,默认情况下,improvedFind.go打印每种类型的文件或目录,并且允许您结合前面的任何标志来打印所需的文件类型。

除了为支持所有这些标志而对main()函数的实现进行的各种更改外,其余大部分更改将发生在walkFunction()函数的代码中。此外,walkFunction()函数将在main()函数中定义,这样做是为了避免使用全局变量。

improvedFind.go的第一部分如下:

package main 

import ( 
   "flag" 
   "fmt" 
   "os" 
   "path/filepath" 
) 

func main() { 

   minusS := flag.Bool("s", false, "Sockets") 
   minusP := flag.Bool("p", false, "Pipes") 
   minusSL := flag.Bool("sl", false, "Symbolic Links") 
   minusD := flag.Bool("d", false, "Directories") 
   minusF := flag.Bool("f", false, "Files") 

   flag.Parse() 
   flags := flag.Args() 

   printAll := false 
   if *minusS && *minusP && *minusSL && *minusD && *minusF { 
         printAll = true 
   } 

   if !(*minusS || *minusP || *minusSL || *minusD || *minusF) { 
         printAll = true 
   } 

   if len(flags) == 0 { 
         fmt.Println("Not enough arguments!") 
         os.Exit(1) 
   } 

   Path := flags[0] 

因此,如果所有标志都未设置,程序将打印所有内容,由第一条if语句处理。同样,如果设置了所有标志,程序也将打印所有内容。因此,需要一个名为printAll的新布尔变量。

improvedFind.go的第二部分有如下 Go 代码,主要是walkFunction变量的定义,实际上是一个函数:

   walkFunction := func(path string, info os.FileInfo, err error) error { 
         fileInfo, err := os.Stat(path) 
         if err != nil { 
               return err 
         } 

         if printAll { 
               fmt.Println(path) 
               return nil 
         } 

         mode := fileInfo.Mode() 
         if mode.IsRegular() && *minusF { 
               fmt.Println(path) 
               return nil 
         } 

         if mode.IsDir() && *minusD { 
               fmt.Println(path) 
               return nil 
         } 

         fileInfo, _ = os.Lstat(path)

         if fileInfo.Mode()&os.ModeSymlink != 0 { 
               if *minusSL { 
                     fmt.Println(path) 
                     return nil 
               } 
         } 

         if fileInfo.Mode()&os.ModeNamedPipe != 0 { 
               if *minusP { 
                     fmt.Println(path) 
                     return nil 
               } 
         } 

         if fileInfo.Mode()&os.ModeSocket != 0 { 
               if *minusS { 
                     fmt.Println(path) 
                     return nil 
               } 
         } 

         return nil 
   } 

在这里,好的方面是,一旦找到匹配项并打印文件,就不必访问其余的if语句,这是将minusF检查放在第一位,将minusD检查放在第二位的主要原因。对os.Lstat()的调用用于确定我们是否正在处理符号链接。这是因为os.Stat()跟随符号链接并返回链接引用的文件的相关信息,而os.Lstat()不这样做:stat(2)lstat(2)也是如此。

您应该非常熟悉improvedFind.go的最后一部分:

   err := filepath.Walk(Path, walkFunction) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 
} 

执行improvedFind.go生成以下输出,是find.go输出的浓缩版本:

$ go run improvedFind.go -d ~/code/C
/home/mtsouk/code/C
/home/mtsouk/code/C/cUNL
/home/mtsouk/code/C/example
/home/mtsouk/code/C/sysProg
/home/mtsouk/code/C/system
$ go run improvedFind.go -sl ~/code
/home/mtsouk/code/aLink

从查找输出中排除文件名

有时您不需要显示从find(1)输出的所有内容。因此,在本小节中,您将学习一种技术,该技术允许您根据文件名手动从improvedFind.go的输出中排除文件。

请注意,此版本的程序不支持正则表达式,仅排除完全匹配的文件名。

因此,将improvedFind.go的改进版本命名为excludeFind.godiff(1)实用程序的输出可以显示improvedFind.goexcludeFind.go之间的代码差异:

$ diff excludeFind.go improvedFind.go
10,19d9
< func excludeNames(name string, exclude string) bool {`
<     if exclude == "" {
<           return false
<     }
<     if filepath.Base(name) == exclude {
<           return true
<     }
<     return false
< }
<
27d16
<     minusX := flag.String("x", "", "Files")
54,57d42
<           if excludeNames(path, *minusX) {
<                 return nil
<           }
<

最重要的变化是引入了一个名为excludeNames()的新 Go 函数,该函数处理文件名排除,并添加了-x标志,用于设置要从输出中排除的文件名。所有作业都是通过文件路径完成的。Base()函数查找路径的最后一部分,即使该路径不是文件而是目录,并将其与-x标志的值进行比较。

请注意,excludeNames()函数更合适的名称可能是isExcluded()或类似名称,因为-x选项接受单个值。

有无-x标志执行excludeFind.go将证明新的 Go 代码实际工作:

$ go run excludeFind.go -x=dT.py ~/code/python
/home/mtsouk/code/python
/home/mtsouk/code/python/dataFile.txt
/home/mtsouk/code/python/python
$ go run excludeFind.go ~/code/python
/home/mtsouk/code/python
/home/mtsouk/code/python/dT.py
/home/mtsouk/code/python/dataFile.txt
/home/mtsouk/code/python/python

从查找输出中排除文件扩展名

文件扩展名是文件名最后一个点(.字符)之后的部分。因此,image.png文件的文件扩展名是 png,它同时适用于文件和目录。

同样,为了实现此功能,您需要一个单独的命令行选项,后跟要排除的文件扩展名:新标志将命名为-ext。此版本的find(1)实用程序将基于excludeFind.go代码,并将命名为finalFind.go。你们中的一些人可能会说,这个选项的更合适的名称应该是-xext,而这一点你们是对的!

再一次,diff(1)实用程序将帮助我们发现excludeFind.gofinalFind.go之间的代码差异:新功能在名为excludeExtensions()的 Go 函数中实现,这使事情更容易理解:

$ diff finalFind.go excludeFind.go
8d7
<     "strings"
21,34d19
< func excludeExtensions(name string, extension string) bool {
<     if extension == "" {
<           return false
<     }
<     basename := filepath.Base(name)
<     s := strings.Split(basename, ".")
<     length := len(s)
<     basenameExtension := s[length-1]
<     if basenameExtension == extension {
<           return true
<     }
<     return false
< }
<
43d27
<     minusEXT := flag.String("ext", "", "Extensions")
74,77d57
<           if excludeExtensions(path, *minusEXT) {
<                 return nil
<           }
< 

当我们寻找路径中最后一个点后的字符串时,我们使用strings.Split()根据路径包含的点字符分割路径。然后,我们获取返回值strings.Split()的最后一部分,并将其与使用-ext标志给出的扩展进行比较。因此,这里没有什么特别之处,只是一些字符串操作代码。再一次,对excludeExtensions()更合适的名称应该是isExcludedExtension()

执行finalFind.go将生成以下输出:

$ go run finalFind.go -ext=py ~/code/python
/home/mtsouk/code/python
/home/mtsouk/code/python/dataFile.txt
/home/mtsouk/code/python/python
$ go run finalFind.go ~/code/python
/home/mtsouk/code/python
/home/mtsouk/code/python/dT.py
/home/mtsouk/code/python/dataFile.txt
/home/mtsouk/code/python/python

使用正则表达式

本节将说明如何在finalFind.go中添加对正则表达式的支持:该工具的最新版本名称为regExpFind.go。新标志将被称为-re,它将需要一个字符串值:任何与此字符串值匹配的内容都将包含在输出中,除非它被另一个命令行选项排除。此外,由于 flags 提供的灵活性,我们不需要删除之前的任何选项来添加另一个选项!

再次,diff(1)命令将告诉我们regExpFind.gofinalFind.go之间的代码差异:

$ diff regExpFind.go finalFind.go
8d7
<     "regexp"
36,44d34
< func regularExpression(path, regExp string) bool {
<     if regExp == "" {
<           return true
<     }
<     r, _ := regexp.Compile(regExp)
<     matched := r.MatchString(path)
<     return matched
< }
<
54d43
<     minusRE := flag.String("re", "", "Regular Expression")
71a61
>
75,78d64
<           if regularExpression(path, *minusRE) == false {
<                 return nil
<           }
< 

第 7 章、**处理系统文件中,我们;我们将进一步讨论 Go 中的模式匹配和正则表达式:现在,理解一下regexp.Compile()创建了一个正则表达式,MatchString()尝试在regularExpression()函数中进行匹配就足够了。

执行regExpFind.go将生成以下输出:

$ go run regExpFind.go -re=anotherPackage /Users/mtsouk/go
/Users/mtsouk/go/pkg/darwin_amd64/anotherPackage.a
/Users/mtsouk/go/src/anotherPackage
/Users/mtsouk/go/src/anotherPackage/anotherPackage.go
$ go run regExpFind.go -ext=go -re=anotherPackage /Users/mtsouk/go
/Users/mtsouk/go/pkg/darwin_amd64/anotherPackage.a
/Users/mtsouk/go/src/anotherPackage 

可以使用以下命令验证以前的输出:

$ go run regExpFind.go /Users/mtsouk/go | grep anotherPackage
/Users/mtsouk/go/pkg/darwin_amd64/anotherPackage.a
/Users/mtsouk/go/src/anotherPackage
/Users/mtsouk/go/src/anotherPackage/anotherPackage.go

创建目录结构的副本

利用您在前面几节中获得的知识,我们现在将开发一个 Go 程序,该程序在另一个目录中创建目录结构的副本:这意味着输入目录中的任何文件都不会复制到目标目录,只会复制目录。当您希望保存其他目录结构中的有用文件,同时保持相同的目录结构,或者手动备份文件系统时,这非常方便。

由于您只对目录感兴趣,cpStructure.go的代码基于您在本章前面看到的traverseDir.go代码:再一次,为学习目的开发的一个小程序帮助您实现一个更大的程序!此外,test选项将显示程序在不实际创建任何目录的情况下将执行的操作。

cpStructure.go的代码将分为四个部分。第一个是:

package main 

import ( 
   "flag" 
   "fmt" 
   "os" 
   "path/filepath" 
   "strings" 
) 

这里没有什么特别的,只是预期的序言。第二部分内容如下:

func main() { 
   minusTEST := flag.Bool("test", false, "Test run!") 

   flag.Parse() 
   flags := flag.Args() 

   if len(flags) == 0 || len(flags) == 1 { 
         fmt.Println("Not enough arguments!") 
         os.Exit(1) 
   } 

   Path := flags[0] 
   NewPath := flags[1] 

   permissions := os.ModePerm 
   _, err := os.Stat(NewPath) 
   if os.IsNotExist(err) { 
         os.MkdirAll(NewPath, permissions) 
   } else { 
         fmt.Println(NewPath, "already exists - quitting...") 
         os.Exit(1) 
   } 

cpStructure.go程序要求目标目录事先不存在,以避免事后出现不必要的意外和错误。

第三部分包含walkFunction变量的代码:

   walkFunction := func(currentPath string, info os.FileInfo, err error) error { 
         fileInfo, _ := os.Lstat(currentPath) 
         if fileInfo.Mode()&os.ModeSymlink != 0 { 
               fmt.Println("Skipping", currentPath) 
               return nil 
         } 

         fileInfo, err = os.Stat(currentPath) 
         if err != nil { 
               fmt.Println("*", err) 
               return err 
         } 

         mode := fileInfo.Mode() 
         if mode.IsDir() { 
               tempPath := strings.Replace(currentPath, Path, "", 1) 
               pathToCreate := NewPath + "/" + filepath.Base(Path) + tempPath 

               if *minusTEST { 
                     fmt.Println(":", pathToCreate) 
                     return nil 
               } 

               _, err := os.Stat(pathToCreate) 
               if os.IsNotExist(err) { 
                     os.MkdirAll(pathToCreate, permissions) 
               } else { 
                     fmt.Println("Did not create", pathToCreate, ":", err) 
               } 
         } 
         return nil 
   } 

在这里,第一个if语句确保我们将处理符号链接,因为符号链接可能是危险的,并且会产生问题:始终尝试处理特殊情况,以避免出现问题和讨厌的 bug。

os.IsNotExist()函数允许您确保您试图创建的目录不存在。因此,如果目录不在那里,您可以使用创建它;os.MkdirAll()os.MkdirAll()函数创建了一个目录路径,其中包含所有必要的父目录,这使得开发人员更简单。

尽管如此,walkFunction变量的代码必须处理的最棘手的部分是删除源路径中不必要的部分并正确构建新路径。程序中使用的strings.Replace()函数将第一个参数(currentPath)中出现的第二个参数(Path)替换为第三个参数(""),替换次数与最后一个参数(1相同。如果最后一个参数是负数,这里不是这样,那么替换的数量将没有限制。在这种情况下,它从正在检查的目录currentPath变量中删除Path变量的值,该变量是源目录。

计划的最后部分如下:

   err = filepath.Walk(Path, walkFunction) 
   if err != nil { 
         fmt.Println(err) 
         os.Exit(1) 
   } 
} 

执行cpStructure.go将生成以下输出:

$ go run cpStructure.go ~/code /tmp/newCode
Skipping /home/mtsouk/code/aLink
$ ls -l /home/mtsouk/code/aLink
lrwxrwxrwx 1 mtsouk mtsouk 14 Apr 21 18:10 /home/mtsouk/code/aLink -> /usr/local/bin 

下图显示了上述示例中使用的源目录和目标目录结构的图形表示:

两个目录结构及其文件的图形表示

练习

  1. 阅读os包的文件页 https://golang.org/pkg/os/
  2. 访问https://golang.org/pkg/path/filepath/ 了解更多关于filepath.Walk()功能的信息。
  3. 更改rm.go的代码以支持多个命令行参数,然后尝试实现rm(1)实用程序的-v命令行选项。
  4. which.go的 Go 代码进行必要的更改,以支持多个命令行参数。
  5. 开始在 Go 中实现一个版本的ls(1)实用程序。不要试图同时支持每个ls(1)选项。
  6. 更改代码traverseDir.go以仅打印常规文件。
  7. 查看find(1)的手册页面,尝试在regExpFind.go中添加对其部分选项的支持。

总结

在本章中,我们讨论了许多事情,包括使用flag标准包,使用 Go 函数处理目录和文件,遍历目录结构,并开发了各种 Unix 命令行实用程序的 Go 版本,包括pwd(1)which(1)rm(1)find(1)

在下一章中,我们将继续讨论文件操作,但这次您将学习如何在 Go 中读取文件和写入文件:正如您将看到的,有很多方法可以做到这一点。虽然这给了你多功能性,但它也要求你能够选择正确的技术来尽可能高效地完成你的工作!因此,您将从学习更多关于io包和bufio包开始,到本章结束时,您将拥有wc(1)dd(1)实用程序的 Go 版本!