15.文件系统
本系列是一个重新学习PowerShell的笔记,内容引用自
PowerShell中文博客
在PowerShell控制台中,文件系统有很特别的重要性。一个明显的原因是管理员需要执行许多涉及文件系统的任务。另一个原因是文件系统是一个层次结构信息模型。在接下来的章节中,你还会看到PowerShell在此基础上控制其它层次信息系统。你可以非常容易的将PowerShell中学到的驱动器,目录和文件的知识点应用到其它地方,其中就包括注册表或者微软的Exchange。
在下面表格中列出的PowerShell 的命令中其全名可能很少有人用到。大家更倾向于使用它们的别名,这些别名来自Windows和Unix系统。
可以让初学者可以非常快速地找到合适的命令。
命令 | 别名 | 描述 |
---|---|---|
cp, cpi | 复制文件或者目录 | Copy-Item |
Dir, ls, gci | 列出目录的内容 | Get-Childitem |
type, cat, gc | 基于文本行来读取内容 | Get-Content |
gi | 获取指定的文件或者目录 | Get-Item |
gp | 获取文件或目录的属性 | Get-ItemProperty |
ii | 使用对应的默认windows程序运行文件或者目录 | Invoke-Item |
— | 连接两个路径为一个路径 | Join-Path |
mi, mv, move | 移动文件或者目录 | Move-Item |
ni | 创建新文件或者目录 | New-Item |
ri, rm, rmdir,del, erase, rd | 删除空目录或者文件 | Remove-Item |
rni, ren | 重命名文件或者路径 | Rename-Item |
rvpa | 处理相对路径或者包含通配符的路径 | Resolve-Path |
sp | 设置文件或路径的属性 | Set-ItemProperty |
Cd,chdir, sl | 更改当前目录的位置 | Set-Location |
— | 提取路径的特定部分,例如父目录,驱动器,文件名 | Split-Path |
— | 测试指定的路径是否存在 | Test-Path |
访问文件和目录
使用Get-ChildItem列出目录的内容。预定义的别名为Dir和ls,Get-ChildItem执行了一些很重要的任务:
- 显示目录内容
- 递归地搜索文件系统查找确定的文件
- 获取文件和目录的对象
- 把文件传递给其它命令,函数或者脚本
注意:因为Windows管理员一般在实践中,使用Get-ChildItem的别名Dir,所以接下来的例子都会使用Dir。另外ls(来自UNIX家族)也可以代替下面例子中的Dir或者Get-ChildItem。
列出目录的内容
一般情况下,你可能只想知道在一个确定的目录中有什么文件,如果你不指定其它参数。Dir会列出当前目录的内容。如果你在Dir后跟了一个目录,它的内容也会被列出来,如果你使用了-recurse参数,Dir会列出所有子目录的内容。当然,也允许使用通配符。
例如,你想列出当前目录下的所有PowerShell脚本,输入下面的命令:
PS C:PowerShell> ls *.ps1
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/15 15:45 397 pipeline.ps1
-a---- 2021/9/15 14:58 36 test.ps1
Dir甚至能支持数组,能让你一次性列出不同驱动器下的内容。下面的命令会同时列出PowerShell根目录下的PowerShell脚本和Windows根目录下的所有日志文件。
PS C:PowerShell> Dir *.ps1, $env:windir*.log
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/15 15:45 397 pipeline.ps1
-a---- 2021/9/15 14:58 36 test.ps1
Directory: C:WINDOWS
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/5/20 19:57 15051 comsetup.log
-a---- 2021/8/10 9:10 240396 DPINST.LOG
-a---- 2019/3/19 12:55 776 DtcInstall.log
-a---- 2021/5/31 9:04 188859 iis.log
-a---- 2021/9/17 18:57 235352 PFRO.log
-a---- 2021/9/8 11:18 25900 setupact.log
-a---- 2021/5/20 20:21 468 setupcomplete.log
-a---- 2021/5/20 19:47 0 setuperr.log
-a---- 2021/6/22 9:19 606 Synaptics.log
-a---- 2021/6/22 9:19 4408 Synaptics.PD.log
-a---- 2021/9/22 13:05 276 WindowsUpdate.log
如果你只对一个目录下的项目名称感兴趣,使用-Name参数,Dir就不会获取对象(Files和directories),只会以纯文本的形式返回它们的名称。
PS C:PowerShell> Dir *.ps1 -Name pipeline.ps1
test.ps1
注意:一些字符在PowerShell中有特殊的意义,比如方括号。方括号用来访问数组元素的。这也就是为什么使用文件的名称会引起歧义。当你使用-literalPath
参数来指定文件的路径时,所有的特殊字符被视为路径片段,PowerShell解释器也不会处理。
Dir 默认的参数为-Path
。假如你当前文件夹下有个文件名为“.a[0].txt
“,因为方括号是PowerShell中的特殊字符,会解释器被解析。为了能正确获取到”.a[0].txt
”的文件信息,此时可以使用-LiteralPath
参数,它会把你传进来的值当作纯文本。
PS C:PowerShell> Get-ChildItem .a[0].txt PS C:PowerShell> Get-ChildItem -Path .a[0].txt PS C:PowerShell> Get-ChildItem -LiteralPath .a[0].txt
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/14 13:51 58 .a[0].txt
递归搜索整个文件系统
当你想搜索整个子目录时,可以使用-recurce参数。但是注意,下面例子在PowerShell2.0版本中执行时会失败。
PS C:PowerShell> Dir *.ps1 -recurse
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/15 15:45 397 pipeline.ps1
-a---- 2021/9/15 14:58 36 test.ps1
PS C:PowerShell> mkdir testdir
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2021/9/22 13:59 testdir
PS C:PowerShell> cd . estdir PS C:PowerShell estdir> "Hua Hua" >test2.ps1 PS C:PowerShell estdir> cd.. PS C:PowerShell> Dir *.ps1 -recurse
Directory: C:PowerShell estdir
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/22 13:59 20 test2.ps1
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/15 15:45 397 pipeline.ps1
-a---- 2021/9/15 14:58 36 test.ps1
你需要了解一点-recurse如何工作的细节来理解为什么会发生上面的情况。Dir总是会获取目录中的内容为文件对象或者目录对象。如果你设置了-recurse开关,Dir会递归遍历目录对象。但是你在上面的例子中使用的通配符只获取扩展名为ps1的文件,没有目录,所以-recurse会跳过。这个概念刚开始使用时可能有点费解,但是下面的使用通配符例子能够递归遍历子目录,正好解释了这点。
在这里,Dir获取了根目录下所有以字母“D”打头的项目。递归开关起了作用,那是因为这些项目中就包含了目录。
Dir $homed* -recurse
过滤和排除标准
现在回到刚开始问题,怎样递归列出同类型的所有文件,比如所有PowerShell scripts。答案是使用Dir完全列出所有目录内容,同时指定一个过滤条件。Dir现在可以过滤出你想要列出的文件了。
PS C:PowerShell> Dir -filter *.ps1 -recurse
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/15 15:45 397 pipeline.ps1
-a---- 2021/9/15 14:58 36 test.ps1
Directory: C:PowerShell estdir
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/22 13:59 20 test2.ps1
除了-filter
,还有一个参数乍一看和-filter
使用起来很像: -include
PS C:PowerShell> Dir C:PowerShell -include *.ps1 -recurse
Directory: C:PowerShell estdir
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/22 13:59 20 test2.ps1
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/15 15:45 397 pipeline.ps1
-a---- 2021/9/15 14:58 36 test.ps1
你会看到这一戏剧性的变化,-filter
的执行效率明显高于-include
:
(Measure-Command {Dir $home -filter *.ps1 -recurse}).TotalSeconds
4,6830099
(Measure-Command {Dir $home -include *.ps1 -recurse}).TotalSeconds
28,1017376
其原因在于-include
支持正则表达式,从内部实现上就更加复杂,而-filter
只支持简单的模式匹配。这也就是为什么你可以使用-include
进行更加复杂的过滤。比如下面的例子,搜索所有第一个字符为A-F的脚本文件,显然已经超出了-filter
的能力范围。
# -filter 查询所有以 "[A-F]"打头的脚本文件,屁都没找到
Dir $home -filter [a-f]*.ps1 -recurse
# -include 能够识别正则表达式,所以可以获取a-f打头,以.ps1收尾的文件
Dir $home -include [a-f]*.ps1 -recurse
与-include
相反的是-exclude
。在你想排除特定文件时,可以使用-exclude
。不像-filter
,-include
和-exclude
还支持数组,能让你获取目录下所选类型的文件。
PS C:PowerShell> Dir C:PowerShell -Recurse -include *.ps1,*.txt
Directory: C:PowerShell estdir
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/22 13:59 20 test2.ps1
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/14 13:51 58 .a[0].txt
-a---- 2021/9/16 13:55 812 Error.txt
-a---- 2021/9/15 15:45 397 pipeline.ps1
-a---- 2021/9/15 14:58 36 test.ps1
做到一点即可:不要混淆了-filter
和 -include
。选择这两个参数中的其中一个:具体为当你的过滤条件没有正则表达式时,使用-filter
,可以显著提高效率。
注意:你不能使用filters在Dir中,列出确定大小的文件列表。因为Dir的限制条件只在文件和目录的名称级别。如果你想使用其它标准来过滤文件,可以尝试第五章中讲到的Where-Object。
下面的例子会获取你家目录下比较大的文件,指定文件至少要100MB大小。
Dir $home -recurse | Where-Object { $_.length -gt 100MB }
如果你想知道Dir返回了多少个文件项,Dir会将结果保存为一个数组,你可以通过数组的的Count属性来读取。下面的命令会告诉你你的家目录下有多少文件(这个操作可能会比较耗时)
PS C:PowerShell> $dd = Dir C:PowerShell -Recurse -include *.ps1,*.txt PS C:PowerShell> $dd
Directory: C:PowerShell estdir
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/22 13:59 20 test2.ps1
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/14 13:51 58 .a[0].txt
-a---- 2021/9/16 13:55 812 Error.txt
-a---- 2021/9/15 15:45 397 pipeline.ps1
-a---- 2021/9/15 14:58 36 test.ps1
PS C:PowerShell> $dd.Count 5
获取文件和目录的内容
你可以使用Dir直接获取一个单独的文件,因为Dir会返回一个目录下所有的文件和目录对象。下面的例子会得到这个文件的FileInfo信息:
PS C:PowerShell> Dir . est.ps1 | Format-List *
PSPath : Microsoft.PowerShell.CoreFileSystem::C:PowerShell est.ps1
PSParentPath : Microsoft.PowerShell.CoreFileSystem::C:PowerShell
PSChildName : test.ps1
PSDrive : C
PSProvider : Microsoft.PowerShell.CoreFileSystem
PSIsContainer : False
Mode : -a----
VersionInfo : File: C:PowerShell est.ps1
InternalName:
OriginalFilename:
FileVersion:
FileDescription:
Product:
ProductVersion:
Debug: False
Patched: False
PreRelease: False
PrivateBuild: False
SpecialBuild: False
Language:
BaseName : test
Target : {}
LinkType :
Name : test.ps1
Length : 36
DirectoryName : C:PowerShell
Directory : C:PowerShell
IsReadOnly : False
Exists : True
FullName : C:PowerShell est.ps1
Extension : .ps1
CreationTime : 2021/9/15 14:13:54
CreationTimeUtc : 2021/9/15 6:13:54
LastAccessTime : 2021/9/15 14:58:02
LastAccessTimeUtc : 2021/9/15 6:58:02
LastWriteTime : 2021/9/15 14:58:02
LastWriteTimeUtc : 2021/9/15 6:58:02
Attributes : Archive
你可以访问单个文件的属性,如果它们的属性支持更改,也可以更改。
PS C:PowerShell> (Dir . est.ps1).BaseName test
PS C:PowerShell> (Dir . est.ps1).FullName C:PowerShell est.ps1
Get-Item
是访问单个文件的另外一个途径, 下面的3条命令都会返回同样的结果:你指定的文件的文件对象。
$file = Dir c:autoexec.bat
$file = Get-Childitem c:autoexec.bat
$file = Get-Item c:autoexec.bat
但是在访问目录而不是文件时,Get-Childitem 和 Get-Item表现迥异。
PS C:PowerShell> $directory =Get-ChildItem C:PowerShell PS C:PowerShell> $directory
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2021/9/22 13:59 testdir
-a---- 2021/9/14 13:51 58 .a[0].txt
-a---- 2021/9/22 11:50 412 employee.xml
-a---- 2021/9/16 13:55 812 Error.txt
-a---- 2021/9/22 11:59 840 namespacexml.xml
-a---- 2021/9/15 15:45 397 pipeline.ps1
-a---- 2021/9/15 10:46 0 test.exe
-a---- 2021/9/15 14:58 36 test.ps1
PS C:PowerShell> $directory =Get-Item C:PowerShell PS C:PowerShell> $directory
Directory: C:
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2021/9/22 13:59 PowerShell
向命令,函数和文件脚本传递文件
因为Dir的结果中返回的是独立的文件或目录对象,Dir可以将这些对象直接交付给其它命令或者你自己定义的函数与脚本。这也使得Dir成为了一个非常重要的的选择命令。使用它你可以非常方便地在一个驱动盘下甚至多个驱动盘下递归查找特定类型的所有文件。
要做到这点,在管道中使用Where-Object
来处理Dir返回的结果,然后再使用ForEach-Object
,或者你自定义的管道过滤。
你还可以将多个Dir 命令执行的结果结合起来。在下面的例子中,两个分开的Dir命令,产生两个分开的文件列表。然后PowerShell将它们结合起来发送给管道进行深度处理。这个例子获取Windows目录和安装程序目录下的所有的dll文件,然后返回这些dll文件的名称,版本,和描述:
$list1 = Dir $env:windirsystem32*.dll
$list2 = Dir $env:programfiles -recurse -filter *.dll
$totallist = $list1 + $list2
$totallist | ForEach-Object {
$info =
[system.diagnostics.fileversioninfo]::GetVersionInfo($_.FullName);
"{0,-30} {1,15} {2,-20}" -f $_.Name, `
$info.ProductVersion, $info.FileDescription
}
因为Dir获取的文件和目录是一样的,有时限制结果中只包含文件或者只包含目录很重要。有很多途径可以做到这点。你可以验证返回对象的属性,PowerShell PSIsContainer属性,或者对象的类型。
# 只列出目录::
Dir | Where-Object { $_ -is [System.IO.DirectoryInfo] }
Dir | Where-Object { $_.PSIsContainer }
Dir | Where-Object { $_.Mode.Substring(0,1) -eq "d" }
# 只列出文件:
Dir | Where-Object { $_ -is [System.IO.FileInfo] }
Dir | Where-Object { $_.PSIsContainer -eq $false}
Dir | Where-Object { $_.Mode.Substring(0,1) -ne "d" }
前面的例子(识别对象类型)是目前速度最快的,而后面的(文本比较)比较复杂和低效。
Where-Object
也可以根据其它属性来过滤。
比如下面的例子通过管道过滤2007年5月12日后更改过的文件:
Dir | Where-Object { $_.CreationTime -gt [datetime]::Parse("May 12, 2007") }
也可以使用相对时间获取2周以内更改过的文件:
Dir | Where-Object { $_.CreationTime -gt (Get-Date).AddDays(-14) }
文件系统导航
除非你通过前面介绍的方式更改了PowerShell控制台的提示信息,否则你工作的当前目录会在控制台的命令行开头显示。你也可以使用Get-Location
或别名pwd
命令获取当前工作的目录。
PS C:PowerShell> Get-Location
Path
----
C:PowerShell
PS C:PowerShell> pwd
Path
----
C:PowerShell
PS C:PowerShell> Get-Alias pwd
CommandType Name Version Source
----------- ---- ------- ------
Alias pwd -> Get-Location
如果你想导航到文件系统的另外一个位置,可以使用Set-Location或者它的别名Cd:
# 进入父目录 (相对路径):
Cd ..
# 进入当前盘的根目录 (相对路径):
Cd
# 进入指定目录 (绝对路径):
Cd c:windows
# 从环境变量中获取系统目录 (绝对路径):
Cd $env:windir
# 从普通变量中获取目录 (绝对路径):
Cd $home
路径的指定可以是相对路径,也可以是绝对路径。在上面的最后一个例子中,兼而有之这两种路径。相对路径依赖你当前的路径,比如. est.txt文件总是指定的是当前目录中的test.txt文件,而.. est.txt指定的是父目录的test.txt文件。相对路径通常比较实用,比如你想使用的脚本库位于当前工作目录,你就可以在不引入其它目录的情况下,直接工作。而绝对路径通常具有唯一性,并且独立于你当前的目录。
相对路径转换成绝对路径
当你使用相对路径时,PowerShell必须将这些相对转换成绝对路径。在你使用相对路径执行一个文件或者一条命令时,该转换会自动发生。你也可以自己使用Resolve-Path
命令来处理。
PS C:PowerShell> Resolve-Path . est.ps1
Path
----
C:PowerShell est.ps1
然而,Resolve-Path
命令只有在文件确实存在时,才会有效。如果你的当前文件夹中没有一个名为a.png
的文件时,Resolve-Path
转换报错。
如果你指定的路径中包含了通配符,Resolve-Path
还可以返回多个结果。下面的命令执行后,会获取PowerShell目录下面的所有的ps1文件的名称。
PS C:PowerShell> Resolve-Path t*.ps1
Path
----
C:PowerShell est.ps1
像Dir一样,Resolve-Path
可以在下行函数中扮演选择过滤器的的角色。下面的例子会演示在记事本中打开一个文件进行处理。命令调用记事本程序通过Resolve-Path
打开这个文件。
notepad.exe (Resolve-Path $pshome ypes.ps1xml).providerpath
如果没有符合标准的文件,Resolve-Path
会抛出一个异常,记录在$?
变量中,在错误发生时表达式!$?
一直会统计,在True
的情况下,代表可能没找到文件。
如果Resolve-Path
找到了多个文件会把它保存在一个数组中,这样的化会有很多不期望的文件被打开。函数使用了之前讲到的PowerShell 内部的函数PromptForChoice()
,来请求用户做出选择。
function edit-file([string]$path = $(Throw "请输入相对路径!")) {
# 处理相对路径,并抑制错误
$files = Resolve-Path $path -ea SilentlyContinue
# 验证是否有错误产生:
if (!$?) {
# 如果是,没有找到符合标准的文件,给出提醒并停止:
"没有找到符合标准的文件.";
break
}
# 如果返回结果为数组,表示有多个文件:
if ($files -is [array]) {
# 此种情况下,列出你想打开的文件:
Write-Host -foregroundColor "Red" -backgroundColor "White" `
"你想打开这些文件吗?"
foreach ($file in $files) {
"- " + $file.Path
}
# 然后确认这些文件是否为用户想打开的:
$yes = ([System.Management.Automation.Host.ChoiceDescription]"&yes")
$no = ([System.Management.Automation.Host.ChoiceDescription]"&no")
$choices = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no)
$result = $host.ui.PromptForChoice('Open files', 'Open these files?', $choices, 1)
# 如果用户确认,使用"&"操作符启动所有的文件
if ($result -eq 0) {
foreach ($file in $files) {
& $file
}
}
}
else {
# 如果是单个文件,可以直接使用"&"启动:
& $files
}
}
保存目录位置
当前的目录可以使用Push-Location
命令保存到目录堆栈的顶部,每一个Push-Location
都可以将新目录添加到堆栈的顶部。使用Pop-Location
可以返回。
因此,如果你要运行一个任务,不得不离开当前目录,可以在运行任务前将用Push-Location
存储当前路径,然后运行结束后再使用Pop-Location
返回到当前目录。Cd $home
总是会返回到你的Home
目录,Push-Location
和 Pop-Location
支持堆栈参数。这使得你可以创建很多堆栈,比如一个任务,一个堆栈。Push-Location -stack job1
会把当前目录保存到job1堆栈中,而不是标准堆栈中。当然在你想重新回到这个位置时,也需要在Pop-Location
中指定这个参数-stack job1
。
查找特殊的目录
Windows使用了很多特殊的目录,根据系统的安装,可能稍有不同。一些非常重要的目录的路径同时也保存在Windows环境变量中,这样PowerShell 可以非常方便和清晰的访问它们。你也可以使用.NET framework中的Environment类去访问其它特殊目录。
特殊目录 | 描述 | 示例 |
---|---|---|
Application data | 存储在本地机器上的应用程序数据 | $env:localappdata |
User profile | 用户目录 | $env:userprofile |
Data used incommon | 应用程序公有数据目录 | $env:commonprogramfiles |
Public directory | 所有本地用户的公有目录 | $env:public |
Program directory | 具体应用程序安装的目录 | $env:programfiles |
Roaming Profiles | 漫游用户的应用程序数据 | $env:appdata |
Temporary files(private) | 当前用户的临时目 录 | $env:tmp |
Temporary files | 公有临时文件目录 | $env:temp |
Windows directory | Windows系统安装的目录 | $env:windir |
环境变量返回的只是其中一部分,还不是全部的特殊目录。比如如果你想将某个文件放到一个用户的桌面,你需要的路径在环境变量中是无法获取的。但是你可以使用.NET的方法environment类下面的GetFolderPath()方法。下面会演示如何在桌面上创建一个快捷方式。
# 在桌面上创建一个快捷方式:
$path = [Environment]::GetFolderPath("Desktop") + "EditorStart.lnk"
$comobject = New-Object -comObject WScript.Shell
$link = $comobject.CreateShortcut($path)
$link.targetpath = "notepad.exe"
$link.IconLocation = "notepad.exe,0"
$link.Save()
GetFolderPath()
目录的类型可以在枚举值SpecialFolder
中找到。你可以使用下面一行脚本查看它的内容。
PS C:PowerShell> [System.Environment+SpecialFolder] | Get-Member -static -memberType Property | select -ExpandProperty Name AdminTools
ApplicationData
CDBurning
CommonAdminTools
CommonApplicationData
CommonDesktopDirectory
CommonDocuments
CommonMusic
CommonOemLinks
CommonPictures
CommonProgramFiles
CommonProgramFilesX86
CommonPrograms
CommonStartMenu
CommonStartup
CommonTemplates
CommonVideos
Cookies
Desktop
DesktopDirectory
Favorites
Fonts
History
InternetCache
LocalApplicationData
LocalizedResources
MyComputer
MyDocuments
MyMusic
MyPictures
MyVideos
NetworkShortcuts
Personal
PrinterShortcuts
ProgramFiles
ProgramFilesX86
Programs
Recent
Resources
SendTo
StartMenu
Startup
System
SystemX86
Templates
UserProfile
Windows
如果你想预览所有GetFolderPath()
支持的目录内容,可以使用下面的例子:
PS C:PowerShell> [System.Environment+SpecialFolder] |
>> Get-Member -static -memberType Property |
>> ForEach-Object { "{0,-25}= {1}" -f $_.name, [Environment]::GetFolderPath($_.Name)
>> }
>AdminTools = C:UsersNB874XEAppDataRoamingMicrosoftWindowsStart MenuProgramsAdministrative Tools
ApplicationData = C:UsersNB874XEAppDataRoaming
CDBurning = C:UsersNB874XEAppDataLocalMicrosoftWindowsBurnBurn
CommonAdminTools = C:ProgramDataMicrosoftWindowsStart MenuProgramsAdministrative Tools
CommonApplicationData = C:ProgramData
CommonDesktopDirectory = C:UsersPublicDesktop
CommonDocuments = C:UsersPublicDocuments
CommonMusic = C:UsersPublicMusic
CommonOemLinks =
CommonPictures = C:UsersPublicPictures
CommonProgramFiles = C:Program FilesCommon Files
CommonProgramFilesX86 = C:Program Files (x86)Common Files
CommonPrograms = C:ProgramDataMicrosoftWindowsStart MenuPrograms
CommonStartMenu = C:ProgramDataMicrosoftWindowsStart Menu
CommonStartup = C:ProgramDataMicrosoftWindowsStart MenuProgramsStartup
CommonTemplates = C:ProgramDataMicrosoftWindowsTemplates
CommonVideos = C:UsersPublicVideos
Cookies = C:UsersNB874XEAppDataLocalMicrosoftWindowsINetCookies
Desktop = C:UsersNB874XEDesktop
DesktopDirectory = C:UsersNB874XEDesktop
Favorites = C:UsersNB874XEFavorites
Fonts = C:WINDOWSFonts
History = C:UsersNB874XEAppDataLocalMicrosoftWindowsHistory
InternetCache = C:UsersNB874XEAppDataLocalMicrosoftWindowsINetCache
构造路径
路径名称由文本构成,能让你随心所欲地构造他们。你也应当看到了上面例子中构造用户桌面快捷方式的过程了:
PS C:PowerShell> $temppath = [Environment]::GetFolderPath("Desktop") + "file.txt" PS C:PowerShell> $temppath C:Users*Desktopfile.txt
一定要确保你的路径中的反斜杠个数正确。这也就是为什么前面的例子中在file.txt前面使用了一个反斜杠。还有一个更可靠的方式,就是使用命令 Join-Path方法,或者.NET中的Path静态类。
PS C:PowerShell> $temppath = Join-Path ([Environment]::GetFolderPath("Desktop")) "test.txt" PS C:PowerShell> $temppath C:Users*Desktop est.txt
PS C:PowerShell> $temppath = [System.IO.Path]::Combine([Environment]::GetFolderPath("Desktop"), "test.txt") PS C:PowerShell> $temppath C:Users*Desktop est.txt
操作目录和文件
Get-ChildItem
和 Get-Item
命令可以获取已经存在的文件和目录。你也可以创建自己的文件和目录,重命名它们,给它们填充内容,复制它们,移动它们,当然也可以删除它们。
创建新目录
创建一个新目录最方便的方式是使用MD
函数它是mkdir
的别名,它内部调用的是New-Item
命令,指定参数–type
的值为Directory
:
PS C:PowerShell> md testdir
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2021/9/24 13:38 testdir
PS C:PowerShell> md . estdirdir1dir2
Directory: C:PowerShell estdirdir1
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2021/9/24 13:38 dir2
PS C:PowerShell> tree.com Folder PATH listing for volume OSDisk
C:.
└───testdir
└───dir1
└───dir2
创建新文件
可能之前你已经使用过New-Item
来创建过文件,但是它们完全是空的:
PS C:PowerShell> New-Item -Name testfile.txt -Type File
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/9/24 13:44 0 testfile.txt
PS C:PowerShell> . estfile.txt PS C:PowerShell> Get-Content . estfile.txt
文件通常会在你保存数据时,自动被创建。因为空文件一般没多大用处。此时重定向和Out-File
,Set-Content
这两个命令可以帮助你:
PS C:PowerShell> dir > . estfile.txt PS C:PowerShell> Get-Content . estfile.txt
Directory: C:PowerShell
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2021/9/24 13:38 testdir
-a---- 2021/9/14 13:51 58 .a[0].txt
-a---- 2021/9/22 11:50 412 employee.xml
-a---- 2021/9/16 13:55 812 Error.txt
-a---- 2021/9/22 11:59 840 namespacexml.xml
-a---- 2021/9/15 15:45 397 pipeline.ps1
-a---- 2021/9/15 10:46 0 test.exe
-a---- 2021/9/15 14:58 36 test.ps1
-a---- 2021/9/24 13:45 0 testfile.txt
PS C:PowerShell> rm . estfile.txt PS C:PowerShell> Set-Content testfile.txt (Get-Date) PS C:PowerShell> Get-Content . estfile.txt 2021/9/24 13:46:34
事实证明在操作上重定向和Out-File
非常的类似:当PowerShell转换管道结果时,文件的内容就像它在控制台上面输出的一样。Set-Content
稍微有所不同。它在文件中只列出目录中文件的名称列表,因为在你使用Set-Content
时,PowerShell不会自动将对象转换成文本输入。相反,Set-Content
会从对象中抽出一个标准属性。上面的情况下,这个属性就是Name了。
通常,你可以将任何文本写入一个文本文件。最后一行演示的是将一个日期对象写入到文件中。比如你手动使用ConvertTo-HTML
将管道结果转换后,Out-File
和Set-Content
会殊途同归。
如果你想决定对象的那个属性应当显示在HTML页面中,可以使用之前提到的Select-Object
在对象转换成HTML前过滤属性。
PS C:PowerShell> dir | Select-Object Name,Length,LastWriteTime | ConvertTo-Html | Out-File testfile.txt PS C:PowerShell> Get-Content . estfile.txt <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>HTML TABLE</title>
</head><body>
<table>
<colgroup><col/><col/><col/></colgroup>
<tr><th>Name</th><th>Length</th><th>LastWriteTime</th></tr>
<tr><td>testdir</td><td></td><td>2021/9/24 13:38:37</td></tr>
<tr><td>.a[0].txt</td><td>58</td><td>2021/9/14 13:51:55</td></tr>
<tr><td>employee.xml</td><td>412</td><td>2021/9/22 11:50:53</td></tr>
<tr><td>Error.txt</td><td>812</td><td>2021/9/16 13:55:55</td></tr>
<tr><td>namespacexml.xml</td><td>840</td><td>2021/9/22 11:59:51</td></tr>
<tr><td>pipeline.ps1</td><td>397</td><td>2021/9/15 15:45:40</td></tr>
<tr><td>test.exe</td><td>0</td><td>2021/9/15 10:46:04</td></tr>
<tr><td>test.ps1</td><td>36</td><td>2021/9/15 14:58:02</td></tr>
<tr><td>testfile.txt</td><td>0</td><td>2021/9/24 13:51:31</td></tr>
</table>
</body></html>
在重定向的过程中,控制台的编码会自动指定特殊字符在文本中应当如何显示。你也可以在使用Out-File命令时,使用-encoding参数来指定。
如果你想将结果导出为逗号分割符列表,可以使用Export-CSV
代替Out-File
。
你可以使用双重定向和Add-Content
向一个文本文件中追加信息。
PS C:PowerShell> Set-Content info.txt "First line" PS C:PowerShell> Get-Content .info.txt First line
PS C:PowerShell> Add-Content .info.txt "Third Line" PS C:PowerShell> Get-Content .info.txt First line
Third Line
PS C:PowerShell> "Second Line" >> .info.txt PS C:PowerShell> Get-Content .info.txt First line
Third Line
S e c o n d L i n e
这个结果让小伙伴们惊呆了:双箭头重定向可以工作,但是文本中显示的字符有间隔。重定向操作符通常使用的是控制台的字符集,如果你的文本中碰巧同时包含了ANSI和Unicode字符集,可能会引起意外的结果。相反,使用Set-Content
,Add-Content
和Out-File
这几条命令,而不使用重定向,可以有效地规避前面的风险。这三条命令都支持-encoding
参数,你可以用它来选择字符集。
创建新驱动器
你可能会惊讶,PowerShell允许你创建新的驱动器。并且不会限制你只创建基于网络的驱动器。你还可以使用驱动器作为你的文件系统中重要目录,甚至你自定义的文件系统的一个快捷方式。
使用New-PSDrive
命令来创建一个新的驱动器。可以像下面那样创建一个网络驱动器。
PS> New-PSDrive -name network -psProvider FileSystem -root \127.0.0.1share
Name Used (GB) Free (GB) Provider Root CurrentLocation
---- --------- --------- -------- ---- ---------------
network FileSystem \127.0.0.1share
PS> dir network:
目录: \127.0.0.1share
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 2013/3/1 14:12 ALwaysOn
d---- 2013/1/4 10:32 Doc
d---- 2013/8/26 13:20 Driver
-a--- 2013/7/10 14:18 7983059 Models.zip
-a--- 2013/5/7 20:44 616 SelectManualCase.ps1
-a--- 2006/10/14 10:18 428832 UISpy.exe
-a--- 2013/3/13 12:23 19435 WMSCloudTopology.xml
在工作目录中创建一个快捷方式也非常方便。下面的命令行会创建一个名为desktop:
和 docs:
的驱动器,它可以代表你的”桌面“目录和Windows目录:“我的文档”
New-PSDrive desktop FileSystem `
([Environment]::GetFolderPath("Desktop")) | out-null
New-PSDrive docs FileSystem `
([Environment]::GetFolderPath("MyDocuments")) | out-null
然后你想更改当前目录为桌面时,只须输入:
Cd desktop:
使用Remove-PSDrive
来删除你创建的驱动器。如果该驱动器正在使用则不能删除。注意在使用New-PSDrive
和Remove-PSDrive
创建或删除驱动器时,指定的字母不能包含冒号,但是在使用驱动器工作时必须指定冒号。
Remove-PSDrive desktop
读取文本文件的内容
使用Get-Content
可以获取文本文件的内容:
PS C:PowerShell> Get-Content .info.txt First line
Third Line
S e c o n d L i n e
如果你知道文件的绝对路径,还可以使用变量符号这个快捷方式读取文本内容:
PS C:PowerShell> &{.info.txt} PS C:PowerShell> &{C:powershellinfo.txt}
通常,这个符号不是很实用,因为在括号中不允许适用任何变量。而大多数情况下绝对路径不会适用所有机器的操作系统。
Get-Content
逐行读取文本的内容,然后把文本的每一行传递给管道。因此,在你想读取一个长文件的前N行,应当适用Select-Object
:
PS C:PowerShell> Get-Content .info.txt | Select-Object -First 1 First line
使用Select-String
可以过滤出文本文件中的信息。下面的命令行会从文件中过滤出包含 third
短语的行。
PS C:PowerShell> Get-Content .info.txt | Select-String "third"
Third Line
处理逗号分隔的列表
在PowerShell中处理逗号分隔的列表文件中的信息时你须要使用Import-Csv
文件。为了测试,先创建一个逗号分隔的文本文件。
PS C:PowerShell> Set-Content user.txt "Username,Function,Passwordage" PS C:PowerShell> Add-Content user.txt "Tobias,Normal,10" PS C:PowerShell> Add-Content user.txt "Martina,Normal,15" PS C:PowerShell> Add-Content user.txt "Cofi,Administrator,-1" PS C:PowerShell> Get-Content user.txt Username,Function,Passwordage
Tobias,Normal,10
Martina,Normal,15
Cofi,Administrator,-1
然后就可以使用Import-Csv输入列表文件了,
PS C:PowerShell> Import-Csv .user.txt
Username Function Passwordage
-------- -------- -----------
Tobias Normal 10
Martina Normal 15
Cofi Administrator -1
如你所见,Import-Csv
理解逗号文件的格式,并且可以逐列显示数据。所以在解析逗号分割的文本文件时,你可以节省下很多工作量:Import-Csv
会替你完成。
第一行被解析成列的标题。然后你就可以将非常方便地将逗号分隔的值作为输入,比如创建用户账号。
PS C:PowerShell> Import-Csv .user.txt | ForEach-Object { $_.Username } Tobias
Martina
Cofi
PS C:PowerShell>
高级主题:除了使用ForEach-Object
循环你还可以在括号中使用脚本块。对于每一个管道内部的管道对象,脚本块都会被执行。在下面的例子中,逗号分割文件中的每一个用户名都会通过echo
的参数-InputObject
返回并输出。
PS C:PowerShell> Import-Csv .user.txt | echo -InputObject {$_.Username } Tobias
Martina
Cofi
解析文本内容和提取文本信息
经常会碰到的一个任务就是解析原始数据,比如日志文件,从所有的数据中获取结构化的目标信息。比如日志文件:windowsupdate.log 它记录了windows更新的细节信息(在之前的例子中我们已经多次用到过这个小白鼠)。该文件还有大量数据,以至于乍一看没什么可读性。初步分析表明该文件是逐行存储的信息,并且每行的信息片段是以Tab字符分割的。
正则表达式为描述这类文件格式提供了最方便的方式,之前已经提到过。
你可以按照下面的例子来使用正则表达式适当地描述文件indowsupdate.log的内容。
# 文本模式包含了6个Tab字符分割的数组
$pattern = "(.*) (.*) (.*) (.*) (.*) (.*)"
# 输入日志
$text = Get-Content $env:windirwindowsupdate.log
# 从日志文件中提取出任意行(这里是第21行)
$text[20] -match $pattern
True
$matches
Name Value
---- -----
6 * Added update {17A5424C-4C70-4BB4-8F83-66DABE5E7CA2}.201 to search result
5 Agent
4 19a4
3 448
2 11:30:42:237
1 2014-02-10
0 2014-02-10 11:30:42:237 448 19a4 Agent * Added update {17A5424C-4C70-4BB4-8F83-66DABE5E7CA2}....
$matches
返回了每个圆括号中定义的子正则表达式的匹配项,这样你就可以使用数字索引来寻址每个文本数组元素了。比如你只对某一行中的日期和描述感兴趣,然后格式化输出它:
PS > "On {0} this took place: {1}" -f $matches[1], $matches[6]
On 2014-02-10 this took place: * Added update {17A5424C-4C70-4BB4-8F83-66DABE5E7CA2}.201 to search result
这种情况下,推荐给每一个子表达式取一个名字,这样可以在后面通过该名字访问。
# 这次子表达式拥有一个名称:
$pattern = "(?<Datum>.*) (?<time>.*) (?<Code1>.*)" + " (?<Code2>.*) (?<Program>.*) (?<Text>.*)"
# 输入日志:
$text = Get-Content $env:windirwindowsupdate.log
# 从日志中提取任意行来解析(这里取第21行):
$text[20] -match $pattern
True
# 从 $matches 中获取信息
# 可以访问指定的名称:
$matches.time + $matches.text
11:30:42:237 * Added update {17A5424C-4C70-4BB4-8F83-66DABE5E7CA2}.201 to search result
现在你可以使用Get-Content
一行一行读取整个日志文件了,然后使用上面的方式逐行处理。这意味着即使在一个庞大的文件中,你也可以快速,相对高效地收集所有你需要的信息。下面的例子正好会列出那些日志行的描述信息中包含了短语“woken up”的文本行。这可以帮助你找出一台机器是否曾经因为自动更新被从待机或者休眠模式唤醒。
Get-Content $env:windirwindowsupdate.log |
ForEach-Object { if ($_ -match "woken up") { $_ } }
2013-05-24 03:00:34:609 1276 1490 AU The machine was woken up by Windows Update
2013-05-24 03:00:34:609 1276 1490 AU The system was woken up by Windows Update, but found to be running on battery power. Skip the forcedinstall.
2013-06-28 03:00:11:563 1272 fe0 AU The machine was woken up by Windows Update
如果进入循环,会将保存在$_
中的完整文本行输出。你现在知道了如何使用正则表达式将一个包含特定信息片段的文本行分割成数组。
然而,还有第二种,更为精妙的方法,从文件中选择个别文本行,它就是Switch
。你只需要告诉语句块,那个文件你想检查,那个模式你想匹配。剩下的工作就交给Switch
吧!下面的语句会获取所有安装的自动更新日志。使用它比之前使用的Get-Content
和ForEach-Object
更快速。你只需要记住正则表达式“.*
”代表任意数量的任意字符。
Switch -regex -file $env:windirwu1.log {'START.*Agent: Install.*AutomaticUpdates' { $_ }}
2013-05-19 09:22:04:113 1248 1d0c Agent **START**
Agent: Installing updates [CallerId = AutomaticUpdates]
2013-05-24 22:31:51:046 1276 c38 Agent **START**
Agent: Installing updates [CallerId = AutomaticUpdates]
2013-06-13 12:05:44:366 1252 228c Agent **START**
Agent: Installing updates [CallerId = AutomaticUpdates]
如果你想找到其它程序的更新,比如SMS或者Defender。只需要在你的正则表达式中使用“SMS”或者“Defender”替换“automatic updates”即可。事实上,Switch
可以接受多个模式,按照下面声明在花括号中的那样,依赖多个模式进行匹配。这就意味着只需几行代码,就可以找出多个程序的更新。
# 为结果创建一个哈希表:
result = @{Defender=0; AutoUpdate=0; SMS=0}
# 解析更新日志,并将结果保存在哈希表中:
Switch -regex -file $env:windirwu1.log
{
'START.*Agent: Install.*Defender' { $result.Defender += 1 };
'START.*Agent: Install.*AutomaticUpdates' { $result.AutoUpdate +=1 };
'START.*Agent: Install.*SMS' { $result.SMS += 1}
}
# 输出结果:
$result
Name Value
---- -----
SMS 0
Defender 1
AutoUpdate 8
读取二进制的内容
不是所有的文件都包含文本。有时,我们需要读取二进制文件中的信息。正常情况下一个文件的扩展名扮演的很重要的角色。因为它决定了Windows使用什么程序来打开这个文件。然而在许多二进制文件中,文件头也紧密的集成到文件中。这些文件头包含了该文件是属于那一类文件的内部类型名称。借助于参数-readCount
和-totalCount
,Get-Content
可以获取这些“魔法字节”。参数-readCount
指明每次读取多少字节,-totalCount
决定了你想从文件中读取的总的字节数。当前情况下,你需要从文件中读取的应当是前4个字节。
function Get-MagicNumber ($path) {
Resolve-Path $path | ForEach-Object {
$magicnumber = Get-Content -encoding byte $_ -read 4 -total 4
$hex1 = ("{0:x}" -f ($magicnumber[0] * 256 + $magicnumber[1])).PadLeft(4, "0")
$hex2 = ("{0:x}" -f ($magicnumber[2] * 256 + $magicnumber[3])).PadLeft(4, "0")
[string] $chars = $magicnumber | % { if ([char]::IsLetterOrDigit($_))
{ [char] $_ } else { "." } }
"{0} {1} '{2}'" -f $hex1, $hex2, $chars
}
}
Get-MagicNumber "$env:windirexplorer.exe"
PS C:PowerShell> . est.ps1
4d5a 9000 'M Z . .'
Explorer的前四个字节为4d, 5a, 90, 和 00或者已经列出的文本MZ。这是Microsoft DOS的开发者之一Mark Zbikowski的简称。所以,标记MZ就代表了可执行的程序。这个标记和图片文件的标记不同:
PS C:PowerShell> Get-MagicNumber "*.png" 8950 4e47 '. P N G'
如你所见,Get-Content
也可以读取二进制文件,一次只读一个字节。参数-readCount
指定每一步读取多少个字节。-totalCount
指定总共要读取的字节数,一旦给它赋值为-1
,它会从头到尾读取所有文件内容。你可以通过将数据输出为十六进制来预览可执行文件。因为纯二进制文本不易阅读。
function Get-HexDump($path, $width = 10, $bytes = -1) {
$OFS = ""
Get-Content -encoding byte $path -readCount $width -totalCount $bytes | ForEach-Object {
$characters = $_
if (($characters -eq 0).count -ne $width) {
$hex = $characters | ForEach-Object {
" " + ("{0:x}" -f $_).PadLeft(2, "0") }
$char = $characters | ForEach-Object {
if ([char]::IsLetterOrDigit($_))
{ [char] $_ } else { "." } }
"$hex $char"
}
}
}
PS C:PowerShell> Get-HexDump .*.png -width 15 -bytes 150 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 .PNG........IHD
52 00 00 02 a7 00 00 01 a9 08 02 00 00 00 3f R..............
65 08 be 00 00 00 09 70 48 59 73 00 00 16 25 e......pHYs....
00 00 16 25 01 49 52 24 f0 00 00 00 11 74 45 .....IR.ð....tE
58 74 53 6f 66 74 77 61 72 65 00 53 6e 69 70 XtSoftware.Snip
61 73 74 65 5d 17 ce dd 00 00 20 00 49 44 41 aste..ÎÝ....IDA
54 78 9c ec bd eb 82 e3 b8 8e 34 18 41 b9 7a Tx.ì.ë.ã..4.A.z
66 f6 41 f6 6d f6 fd 1f 65 bf e9 4a 21 be 1f föAömöý.e.éJ...
00 49 90 a2 6c 39 d3 79 a9 3e 8d 73 3a cb 96 .I..l9Óy...s.Ë.
25 de 44 e2 12 00 41 e2 ff fd ff 48 02 10 0e .ÞDâ..AâÿýÿH...
移动和复制文件和目录
Move-Item
和 Copy-Item
用来执行移动和拷贝操作。它们也支持通配符。比如下面的脚本会将你家目录下的的所有PowerShell脚本文件复制到桌面上:
PS C:PowerShell> Copy-Item *.ps1 ([Environment]::GetFolderPath("Desktop"))
但是,只有在当前目录当下的脚本会被复制。幸亏Copy-Item
还有一个参数-recurse
,这个参数的效果类似Dir中的效果。如果你的初始化目录不包含任何目录,它也不会工作。
Copy-Item -recurse $home*.ps1 ([Environment]::GetFolderPath("Desktop"))
使用Dir也可以复制所有PowerShell脚本到你的桌面,让我们先给你找出这些脚本,然后将结果传递给Copy-Item:
Dir -filter *.ps1 -recurse | ForEach-Object {
Copy-Item $_.FullName ([Environment]::GetFolderPath("Desktop")) }
小技巧:你可能想要缩减脚本行,因为文件对象整合了一个CopyTo()方法。
Dir -filter *.ps1 -recurse | ForEach-Object {
$_.CopyTo([Environment]::GetFolderPath("Desktop")) }
但是结果可能会出错,因为CopyTo()是一个低级的函数。它需要文件的目标路径也被复制。因为你只是想复制所有文件到桌面,你已经指定了目标路径的目录。CopyTo()会尝试将文件复制这个精确的字符串路径(桌面)下,但是肯定不会得逞,因为桌面是一个已经存在的目录了。相反的Copy-Item就聪明多了:如果目标路径是一个目录,它就会把文件复制到这个目录下。
此时,你的桌面上可能已经堆满了PowerShell脚本,最好的方式是将它们保存到桌面的一个子目录中。你需要在桌面上创建一个新目录,然后从桌面到这个子目录中移动所有的脚本。
$desktop = [Environment]::GetFolderPath("Desktop")
md ($desktop + "PS Scripts")
Move-Item ($desktop + "*.ps1") ($desktop + "PS Scripts")
此时,你的桌面又恢复了往日的整洁,也把脚本安全的保存到桌面了。
重命名文件和目录
使用Rename-Item
你可以给文件或者目录换个名字。但是这样做时要格外小心,因为如果把某些系统文件给重命名了,可能会导致系统瘫痪。甚至你只是更改了某些文件的扩展名,也会导致它们不能正常打开或者显示它们的一些属性。
Set-Content testfile.txt "Hello,this,is,an,enumeration"
# 在默认编辑器中打开文件:
. estfile.txt
# 在Excel中打开文件:
Rename-Item testfile.txt testfile.csv
. estfile.csv
批量重命名
因为Rename-Item
可以在管道中的语句块中使用,这就给一些复杂的任务提供了令人惊讶的方便的解决方案。比如,你想将一个目录的名称和它的子目录的名称,包括目录下的文件的名称中所有的“x86”词语移除掉。下面的命令就够了:
Dir | ForEach-Object {
Rename-Item $_.Name $_.Name.replace("-x86", "") }
然而,上面的命令会实际上会尝试重命名所有的文件和目录,即使你找的这个词语在文件名中不存在。产生错误并且非常耗时。为了大大提高速度,可是使用Where-Object
先对文件名进行过滤,然后对符合条件的文件进行重命名。
Dir | Where-Object { $_.Name -contains "-x86" } | ForEach-Object {
Rename-Item $_.Name $_.Name.replace("-x86", "") }
更改文件扩展名
如果你想更改文件的扩展名,首先需要意识到后果:文件随后会识别为其它文件类型,而且可能被错误的应用程序打开,甚至不能被任何应用程序打开。下面的命令会把当前文件夹下的所有的PowerShell脚本的后缀名从“.ps1”改为“.bak”。
Dir *.ps1 | ForEach-Object { Rename-Item $_.Name `
([System.IO.Path]::GetFileNameWithoutExtension($_.FullName) + `
".bak") -whatIf }
What if: Performing operation "Rename file" on Target
"Element: C:UsersTobias Weltner abexpansion.ps1
Destination: C:UsersTobias Weltner abexpansion.bak".
由于-whatIf
参数的缘故,一开始语句只会表明可能会执行重命名操作。
整理文件名
数据集往往随着时间的增长而增长。如果你想整理一个目录,你可以给定所有的文件一个统一的名称和序号。你可以从文件的某些具体的属性中合成文件名。还记得上面在桌面上为PowerShell脚本创建的那个子目录吗?让我们对它里面的PowerShell脚本以数字序号重命名吧。
Dir $directory*.ps1 | ForEach-Object {$x=0} {
Rename-Item $_ ("Script " + $x + ".ps1"); $x++ } {"Finished!"}
Dir $directory*.ps1
删除文件和目录
使用Remove-Item
和别名Del
可以删除文件和目录,它会不可恢复的删除文件和目录。如果一个文件属于只读文件,你需要指定参数-force
:
# 创建示例文件:
$file = New-Item testfile.txt -type file
# 文件不是只读:
$file.isReadOnly
False
# 激活只读属性:
$file.isReadOnly = $true
$file.isReadOnly
True
# 只读的文件需要指定-Force参数才能顺利删除:
del testfile.txt
Remove-Item : Cannot remove item C:UsersTobias Weltner estfile.txt: Not enough permission to perform operation.
At line:1 char:4
+ del <<<< testfile.txt
del testfile.txt -force
Table
删除目录内容
如果一个目录被删除了,它里面所有的内容都会丢失。在你尝试去删除一个文件夹连同它的内容时,PowerShell都会请求你的批准。这样是为了防止你无意间销毁大量数据。只有空目录才不需要请求确认信息。
# 新建一个测试目录:
md testdirectory
Directory: Microsoft.PowerShell.CoreFileSystem::C:UsersTobias WeltnerSourcesdocs
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 13.10.2007 13:31 testdirectory
# 在目录中新建一个文件
Set-Content . estdirectory estfile.txt "Hello"
# 删除目录 directory:
del testdirectory
Confirm
The item at "C:UsersTobias WeltnerSourcesdocs estdirectory" has children
and the Recurse parameter was not specified. If you continue, all children
will be removed with the item. Are you sure you want to continue?
|Y| Yes |A| Yes to All |N| No |L| No to All |S| Suspend |?| Help (default is"Y"):
但是,如果你指定了参数-recurse
:PowerShell会将这个目录连同它里面的内容删除,没有任何确认提示。
管理访问权限
对于NTFS驱动器来说,访问权限决定着那个用户可以访问文件和目录。对于每一个文件和文件夹,所谓的安全描述符(SD)规定了安全数据。安全描述符决定安全设置是否只对当前目录有效,或者它可以被传递给其它文件和目录。真正的访问权限是在访问控制列表(ACL)中。每一个访问权限的访问控制项(ACE)也在ACL中。
注意:文件和目录访问权限相当于一个复杂的电子锁。如果使用得当,你可以把它变成一个有力的安全系统。然而,如果使用不当,你可能很容易把自己锁在外面,失去了访问重要数据的权限,或者破坏了Windows操作系统(当你无意间禁止了访问关键系统目录的权限后)。作为文件和目录的所有者,你总是有更正权限的选项;作为一个管理员,你也总能取得文件和目录的拥有权。但这是不得已的后门,你不能依赖它:你应当在你能意识到后果的情况下更改权限。最好一开始使用测试文件和目录做实验。
PowerShell使用Get-Acl
命令 Set-Acl
来管理权限。此外,类似cacls
这样的传统命令也可以在PowerShell的控制台上面使用。通常他们更改起来访问权限会比PowerShell命令更快。尤其在你处理非常多的文件和目录时。由于Windows Vista的发布,cacls
一直被视为过时。如果可能的化,你可以使用它的继任者icacls
。
PS> icacls /?
ICACLS name /save aclfile [/T] [/C] [/L] [/Q]
将匹配名称的文件和文件夹的 DACL 存储到 aclfile 中
以便将来与 /restore 一起使用。请注意,未保存 SACL、
所有者或完整性标签。
ICACLS directory [/substitute SidOld SidNew [...]] /restore aclfile
[/C] [/L] [/Q]
将存储的 DACL 应用于目录中的文件。
ICACLS name /setowner user [/T] [/C] [/L] [/Q]
更改所有匹配名称的所有者。该选项不会强制更改所有
身份;使用 takeown.exe 实用程序可实现
该目的。
ICACLS name /findsid Sid [/T] [/C] [/L] [/Q]
查找包含显式提及 SID 的 ACL 的
所有匹配名称。
ICACLS name /verify [/T] [/C] [/L] [/Q]
查找其 ACL 不规范或长度与 ACE
计数不一致的所有文件。
ICACLS name /reset [/T] [/C] [/L] [/Q]
为所有匹配文件使用默认继承的 ACL 替换 ACL。
ICACLS name [/grant[:r] Sid:perm[...]]
[/deny Sid:perm [...]]
[/remove[:g|:d]] Sid[...]] [/T] [/C] [/L] [/Q]
[/setintegritylevel Level:policy[...]]
/grant[:r] Sid:perm 授予指定的用户访问权限。如果使用 :r,
这些权限将替换以前授予的所有显式权限。
如果不使用 :r,这些权限将添加到以前授予的
所有显式权限。
/deny Sid:perm 显式拒绝指定的用户访问权限。
将为列出的权限添加显式拒绝 ACE,
并删除所有显式授予的权限中的相同权限。
/remove[:[g|d]] Sid 删除 ACL 中所有出现的 SID。使用
:g,将删除授予该 SID 的所有权限。使用
:d,将删除拒绝该 SID 的所有权限。
/setintegritylevel [(CI)(OI)]级别将完整性 ACE 显式
添加到所有匹配文件。要指定的级别为以下级别
之一:
L[ow]
M[edium]
H[igh]
完整性 ACE 的继承选项可以优先于级别,但只应用于
目录。
/inheritance:e|d|r
e - 启用继承
d - 禁用继承并复制 ACE
r - 删除所有继承的 ACE
注意:
Sid 可以采用数字格式或友好的名称格式。如果给定数字格式,
那么请在 SID 的开头添加一个 *。
/T 指示在以该名称指定的目录下的所有匹配文件/目录上
执行此操作。
/C 指示此操作将在所有文件错误上继续进行。
仍将显示错误消息。
/L 指示此操作在符号
链接本身而不是其目标上执行。
/Q 指示 icacls 应该禁止显示成功消息。
ICACLS 保留 ACE 项的规范顺序:
显式拒绝
显式授予
继承的拒绝
继承的授予
perm 是权限掩码,可以指定两种格式之一:
简单权限序列:
N - 无访问权限
F - 完全访问权限
M - 修改权限
RX - 读取和执行权限
R - 只读权限
W - 只写权限
D - 删除权限
在括号中以逗号分隔的特定权限列表:
DE - 删除
RC - 读取控制
WDAC - 写入 DAC
WO - 写入所有者
S - 同步
AS - 访问系统安全性
MA - 允许的最大值
GR - 一般性读取
GW - 一般性写入
GE - 一般性执行
GA - 全为一般性
RD - 读取数据/列出目录
WD - 写入数据/添加文件
AD - 附加数据/添加子目录
REA - 读取扩展属性
WEA - 写入扩展属性
X - 执行/遍历
DC - 删除子项
RA - 读取属性
WA - 写入属性
继承权限可以优先于每种格式,但只应用于
目录:
(OI) - 对象继承
(CI) - 容器继承
(IO) - 仅继承
(NP) - 不传播继承
(I) - 从父容器继承的权限
示例:
icacls c:windows* /save AclFile /T
- 将 c:windows 及其子目录下所有文件的
ACL 保存到 AclFile。
icacls c:windows /restore AclFile
- 将还原 c:windows 及其子目录下存在的 AclFile 内
所有文件的 ACL。
icacls file /grant Administrator:(D,WDAC)
- 将授予用户对文件删除和写入 DAC 的管理员
权限。
icacls file /grant *S-1-1-0:(D,WDAC)
- 将授予由 sid S-1-1-0 定义的用户对文件删除和
写入 DAC 的权限。
检查有效的安全设置
文件和目录的有效安全设置在访问控制列表中,使用Get-Acl
时,会获取列表中的内容。因此如果你想找出谁能够访问某些文件或者目录,可以这样处理:
PS C:PowerShell> Get-Acl $env:windir
Directory: C:
Path Owner Access
---- ----- ------
WINDOWS NT SERVICETrustedInstaller CREATOR OWNER Allow 268435456...
确认文件所有者的身份
文件和目录的所有者还有一些特殊的权限。比如文件的所有者总是能够访问文件。你可以通过Owner属性,来获取所有者名称。
PS C:PowerShell> (Get-Acl $env:windir).Owner NT SERVICETrustedInstaller
PS C:PowerShell> $acl = (Get-Acl $env:windir) PS C:PowerShell> $acl.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False DirectorySecurity System.Security.AccessControl.FileSystemSecurity
PS C:PowerShell> $acl | fl *
PSPath : Microsoft.PowerShell.CoreFileSystem::C:WINDOWS
PSParentPath : Microsoft.PowerShell.CoreFileSystem::C:
PSChildName : WINDOWS
PSDrive : C
PSProvider : Microsoft.PowerShell.CoreFileSystem
CentralAccessPolicyId :
CentralAccessPolicyName :
Path : Microsoft.PowerShell.CoreFileSystem::C:WINDOWS
Owner : NT SERVICETrustedInstaller
Group : NT SERVICETrustedInstaller
...
列出访问权限
实际上访问权限就是——谁可以做什么,下面输出访问属性:
PS C:PowerShell> $acl.Access | ft -Wrap
FileSystemRights AccessControlType IdentityReference IsInherited InheritanceFlags PropagationFlags
---------------- ----------------- ----------------- ----------- ---------------- ----------------
268435456 Allow CREATOR OWNER False ContainerInherit, ObjectInherit InheritOnly
268435456 Allow NT AUTHORITYSYSTEM False ContainerInherit, ObjectInherit InheritOnly
Modify, Synchronize Allow NT AUTHORITYSYSTEM False None None
268435456 Allow BUILTINAdministrators False ContainerInherit, ObjectInherit InheritOnly
Modify, Synchronize Allow BUILTINAdministrators False None None
-1610612736 Allow BUILTINUsers False ContainerInherit, ObjectInherit InheritOnly
ReadAndExecute, Synchronize Allow BUILTINUsers False None None
268435456 Allow NT SERVICETrustedInstaller False ContainerInherit InheritOnly
FullControl Allow NT SERVICETrustedInstaller False None None
ReadAndExecute, Synchronize Allow APPLICATION PACKAGE AUTHORITYALL APPLICATION PACKAGES False None None
-1610612736 Allow APPLICATION PACKAGE AUTHORITYALL APPLICATION PACKAGES False ContainerInherit, ObjectInherit InheritOnly
ReadAndExecute, Synchronize Allow APPLICATION PACKAGE AUTHORITYALL RESTRICTED APPLICATION PACKAGES False None None
-1610612736 Allow APPLICATION PACKAGE AUTHORITYALL RESTRICTED APPLICATION PACKAGES False ContainerInherit, ObjectInherit InheritOnly
在上面表格的IdentityReference
列,告诉你谁有特殊的权限。FileSystemRights
列告诉你权限的类型。AccessControlType
列格外重要,如果它显示“拒绝”而不是“允许”,你懂的,它会限制用户访问。
创建新的权限
Get-Acl
执行后返回的对象,包含若干方法可以用来更新权限和设定所有权。如果你只想设定自己的权限,都没必要去安全描述符世界深究。往往,读取一个已经存在的文件安全描述符,把它传递给另一个文件,或者按照特殊SDDL语言文字的形式指定安全信息就够了。
技巧
:下面的例子会让你认识一些日常步骤。注意两点即可:别忘了cacls
这个可靠的工具,因为使用它会比PowerShell命令更高效。此外,Get-ACL
和Set-ACL
不仅仅应用于文件层面,还可以用于其它有访问控制的安全描述符的任何地方,比如Windows注册表(会在下一章讲解)
克隆权限
在一个初级的案例中,你可能都不会创建任何新的权限,只会从一个已经存在的文件或者目录的访问控制列表中克隆一个权限,然后把它转让给其它文件。优点是可以使用图形用户界面来设置那些通常比较复杂的权限。
开始之前,先创建两个目录作为测试:
md Prototype | out-null
md Protected | out-null
现在,打开资源管理器,设置Prototype目录的安全设置。
explorer .
在资源管理器中,右击Prototype目录,选择属性,然后点击安全选项卡,点击编辑。通过添加其他用户来更改测试目录的安全设置。在下面的对话框中给新用户设置权限。
注意:你也可以通过勾选拒绝复选框来拒绝用户的权限。这样做时,可要留心了。因为限制权限总是有高优先级。比如,你给了自己完全控制的权限,但是拒绝了“Everyone”这个组来访问。这样就把自己关在文件系统的外面了。因为你也属于”Everyone”这个组,同时因为限制的优先级比较高,哪怕你已经给了自己“完全控制”的权限,这个限制也作用于你。
你更改了权限后,捎带在资源管理器中看看第二个目录Protected。这个目录仍旧是默认赋予的权限。下一步,我们会把Prototype刚才设置的权限转交到Protected目录。
$acl = Get-Acl Prototype
Set-Acl Protected $acl
注意:你本身需要特殊的权限去设置上面的权限。如果你用的是Windows Vista操作系统,并且启用了UAC,使用PowerShell操作时,会出现错误,提示你没有权限。这时可以通过让控制台以管理员权限运行来获取权限。
实验做完了,现在呢,Protected和Prototype一样安全。当你在资源管理器中查看它们的安全设置时,你会发现所有的设置都是相同的。
使用SDDL设置权限
前面的例子非常简单,你所做的只是把已有目录的安全设置移交给其它目录。在你的日常工作中,你可能得具备一个你根本就不需要的Prototype目录。但是你可以通过文本格式的安全描述符来归纳安全设置。每一个安全设置都是被特殊的安全描述符描述语言(SDDL
)定义的。它能让你以文本的形式读取Prototype目录的安全信息,以后无须借助Prototype目录即可使用。
让我们删掉这个测试目录Protected吧,然后在SDDL
中保存Prototype目录的安全信息。
PS C:PowerShell> $acl = (Get-Acl . estdir)
PS C:PowerShell> $sddl = $acl.Sddl
PS C:PowerShell> $sddl
O:S-1-5-21-3190493677-272540594-4000629471-598229G:S-1-5-21-3190493677-272540594-4000629471-513D:AI(A;OICIID;FA;;;BA)(A;OICIID;FA;;;SY)(A;OICIID;0x1200a9;;;BU)(A;ID;0x1301bf;;;AU)(A;OICIIOID;SDGXGWGR;;;AU)
然后把这个SDDL
文本保存到第二个脚本中,可将该安全设置赋给任意目录。
# 创建新目录
Md Protected
# 在 SDDL中的是安全描述符 (一行):
$sddl = "O:S-1-5-21-3190493677-272540594-4000629471-598229G:S-1-5-21-3190493677-272540594-4000629471-513D:AI(A;OICIID;FA;;;BA)(A;OICIID;FA;;;SY)(A;OICIID;0x1200a9;;;BU)(A;ID;0x1301bf;;;AU)(A;OICIIOID;SDGXGWGR;;;AU)"
# 获取目录的安全描述:
$acl = Get-Acl Protected
# 使用SDDL定义替换安全描述 :
$acl.SetSecurityDescriptorSddlForm($sddl)
# 保存更新
Set-Acl Protected $acl
注意:你的第二个目录是完全独立于Prototype目录的。你所需要做的可能是,借助Prototype目录使用图形用户界面,临时生成一个SDDL安全设置定义。
然而,SDDL
不能很方便的移交给其它机器。如果你仔细看下,每个授权用户不是根据用户名识别,而是根据它们的安全标识符(SID
)识别。不同的机器上,即使用户名相同,这个SID
也不会相同,因为它们隶属不同的账户。但是在一个域(domain
)中,相同名字的账号的SID
是相同的,因为域会集中管理。其结果就是SDDL
解决方案在基于域环境的公司网络中非常完美。
尽管如此,如果你处在一个小型的对等网络中,SDDL
也能非常有用。你只需要使用“复制黏贴”去替换SID而已。不过,在对等网络中,cacls
或者 icacls
命令可能更简单一点。
手动创建新权限
权限也可以被手动创建。其优点就是,即使没有集中域,你也可以根据用户名来指定授权用户,这样可以以相同的方式在任意机器上工作。但是注意,它引入了额外的工作,因为你必须完全创建你自己的安全描述符,接下来的例子会展示。但是在实践中发现这个过程非常的耗时。使用cacls和icacls都比它简单一点。现在我们删除掉测试目录Protected,再次创建一个新的目录,让它只有默认的访问权限。
$acl = Get-Acl Protected
# 添加第一个规则:
$person = [System.Security.Principal.NTAccount]"Administrator"
$access = [System.Security.AccessControl.FileSystemRights]"FullControl"
$inheritance = [System.Security.AccessControl.InheritanceFlags] "ObjectInherit,ContainerInherit"
$propagation = [System.Security.AccessControl.PropagationFlags]"None"
$type = [System.Security.AccessControl.AccessControlType]"Allow"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( $person,$access,$inheritance,$propagation,$type)
$acl.AddAccessRule($rule)
# 添加第二个规则:
$person = [System.Security.Principal.NTAccount]"Everyone"
$access = [System.Security.AccessControl.FileSystemRights]"ReadAndExecute"
$inheritance = [System.Security.AccessControl.InheritanceFlags] "ObjectInherit,ContainerInherit"
$propagation = [System.Security.AccessControl.PropagationFlags]"None"
$type = [System.Security.AccessControl.AccessControlType]"Allow"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( $person,$access,$inheritance,$propagation,$type)
$acl.AddAccessRule($rule)
# 保存权限更新:
Set-Acl Protected $acl
接下来,让我们一起看看每个访问规则是怎么定义的。每一个规则需要5个细节:
- Person:这是该规则应当适用的人或者组。
- Access:这里选择规则要控制的权限。
- Inheritance:这里选择规则要应用的对象。这个规则能够,并且一般是会授予它的子对象,这样它就能自动适用于目录中的文件了。
- Propagation:决定权限是否要传递给子对象(比如子目录和文件),通常情况下设置为None,仅仅授予权限。
- Type:它能让你设置权限或者限制,如果限制,指定的权限会明确不予批准。
接下来问题是这些规范允许那些值?这个例子演示通过.NET对象(第六章)显示这些规范。你可以使用下面的机器列出访问权限允许的值:
PS C:PowerShell> [System.Enum]::GetNames([System.Security.AccessControl.FileSystemRights])
ListDirectory
ReadData
WriteData
CreateFiles
CreateDirectories
AppendData
ReadExtendedAttributes
WriteExtendedAttributes
Traverse
ExecuteFile
DeleteSubdirectoriesAndFiles
ReadAttributes
WriteAttributes
Write
Delete
ReadPermissions
Read
ReadAndExecute
Modify
ChangePermissions
TakeOwnership
Synchronize
FullControl
如果你想设置权限时,实际上得结合上面列表中列出的相关值,比如:
PS C:PowerShell> $access = [System.Security.AccessControl.FileSystemRights]::Read `
>> -bor [System.Security.AccessControl.FileSystemRights]::Write
PS C:PowerShell> $access
Write, Read
结果是一个数字,读和写权限的位掩码。在上面的例子中,你可以非常简单第获取相同的结果,因为允许你指定你想要的项目,甚至把它们放在一个逗号分隔项中,紧跟在括号括起来的.NET枚举类型后面。
PS C:PowerShell> $access = [System.Security.AccessControl.FileSystemRights]"Read,Write"
PS C:PowerShell> $access
Write, Read
PS C:PowerShell> [int]$access
131487
因为这里你没有指定二进制计算符-bor
,它的结果是可读的文本。而此时需要位掩码来工作,所以把它转换成Integer整形数据类型。你可以像这样随时得出设置的相关值。
PS C:PowerShell> [int][System.Security.AccessControl.InheritanceFlags] `
>> "ObjectInherit,ContainerInherit"
3
这样做的意义在于你现在可以测试其它.NET枚举类型的值,把它们转换成整数。虽然不能增强你的命令的可读性,但是可以压缩脚本。因为下面的脚本行和前面例子中的脚本行可以做同一件事。
Del Protected
Md Protected
$acl = Get-Acl Protected
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( "Administrator",2032127,3,0,0)
$acl.AddAccessRule($rule)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( "Everyone",131241,3,0,0)
$acl.AddAccessRule($rule)
# 保存更新的权限:
Set-Acl Protected $acl
最后,我们看看PowerShell是怎么指定特定用户的权限的。在上面的例子中,你指定了用户或者组的名称,但是权限不能识别用户名,但能识别账号的唯一SID
,用户名在内部会被更改成SID
,你也可以在脚本中手动更改用户名,看看指定的用户名是否存在。
PS C:PowerShell> $Account = [System.Security.Principal.NTAccount]"Administrators"
PS C:PowerShell> $SID = $Account.translate([System.Security.Principal.Securityidentifier])
PS C:PowerShell> $SID
BinaryLength AccountDomainSid Value
------------ ---------------- -----
16 S-1-5-32-544
一个NTAccount
对象描述了一个权限可以分配的安全主体。在实践中,它是用户和组。NTAccount
对象可以使用Translate()
来输出它包含的与主体对应的SID
。而这只会在指定的账号确实存在的情况下有效。否则,你会得到一个错误。因此你也可以使用Translate()
来验证一个账号的存在性。
通过Translate()
获取的SID非常有用。如果你仔细看,你会发现管理员组的SID和你自己当前账号的SID完全不同:
PS C:PowerShell> ([System.Security.Principal.NTAccount]"$env:userdomain$env:username").`
>> Translate([System.Security.Principal.Securityidentifier]).Value
S-1-5-21-3190493677-272540594-4000629471-598229
PS C:PowerShell> ([System.Security.Principal.NTAccount]"Administrators").`
>> Translate([System.Security.Principal.Securityidentifier]).Value
S-1-5-32-544
PS C:PowerShell>
管理员组的SID
不但很短,而且是唯一的。为了整合这个账号,Windows使用了所谓的众所周知的SID
,它在所有的Windows系统中都是相同的。这一点很重要,因为你德文系统中运行上面的脚本会失败。在本地化的德文系统上,因为Administrators
组叫做”Administratoren
”,”Everyone
”组叫做”Jeder
”。但是这些账号的SID是相同的。知道了这些组的SID号,你就可以使用它们代替那些本地化的名称了。下面是怎样将SID转换成用户账号的名称:
PS C:PowerShell> $sid = [System.Security.Principal.SecurityIdentifier]"S-1-5-32-544" PS C:PowerShell> $sid.Translate([System.Security.Principal.NTAccount])
Value
-----
BUILTINAdministrators
如何让你的脚本能够非常完美地在国际本地化机器上运行:
Del Protected
Md Protected
$acl = Get-Acl Protected
# 管理员完全控制:
$sid = [System.Security.Principal.SecurityIdentifier]"S-1-5-32-544"
$access = [System.Security.AccessControl.FileSystemRights]"FullControl"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( `
$sid,$access,3,0,0)
$acl.AddAccessRule($rule)
# 所有用户的只读权限:
$sid = [System.Security.Principal.SecurityIdentifier]"S-1-1-0"
$access = [System.Security.AccessControl.FileSystemRights]"ReadAndExecute"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule( `
$sid,$access,3,0,0)
$acl.AddAccessRule($rule)
# 保存权限更新:
Set-Acl Protected $acl