PowerShell命令与脚本(14)——文件系统

前言

在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脚本,输入下面的命令:
Dir *.ps1
Dir甚至能支持数组,能让你一次性列出不同驱动器下的内容。下面的命令会同时列出PowerShell根目录下的PowerShell脚本和Windows根目录下的所有日志文件。
Dir $pshome\*.ps1, $env:windir\*.log
如果你只对一个目录下的项目名称感兴趣,使用-Name参数,Dir就不会获取对象(Files和directories),只会以纯文本的形式返回它们的名称。
Dir -name
 
注意:一些字符在PowerShell中有特殊的意义,比如方括号。方括号用来访问数组元素的。这也就是为什么使用文件的名称会引起歧义。当你使用-literalPath参数来指定文件的路径时,所有的特殊字符被视为路径片段,PowerShell解释器也不会处理。
 
注:Dir 默认的参数为-Path。假如你当前文件夹下有个文件名为“.\a[0].txt“,因为方括号是PowerShell中的特殊字符,会解释器被解析。为了能正确获取到”.\a[0].txt”的文件信息,此时可以使用-LiteralPath参数,它会把你传进来的值当作纯文本。
 
递归搜索整个文件系统
当你想搜索整个子目录时,可以使用-recurce参数。
Dir获取了根目录下所有以字母“D”打头的项目。递归开关起了作用,那是因为这些项目中就包含了目录。
Dir $home\d* -recurse
 
过滤和排除标准
现在回到刚开始问题,怎样递归列出同类型的所有文件,比如所有PowerShell scripts。答案是使用Dir完全列出所有目录内容,同时指定一个过滤条件。Dir现在可以过滤出你想要列出的文件了。
Dir -filter *.ps1 -recurse
除了-filter,还有一个参数乍一看和-filter使用起来很像: -include
Dir -include *.ps1 -recurse
你会看到这一戏剧性的变化,-filter的执行效率明显高于-include:
其原因在于-include支持正则表达式,从内部实现上就更加复杂,而-filter只支持简单的模式匹配。这也就是为什么你可以使用-include进行更加复杂的过滤。比如下面的例子,搜索所有第一个字符为A-F的脚本文件,显然已经超出了-filter的能力范围。
与-include相反的是-exclude。在你想排除特定文件时,可以使用-exclude。不像-filter,-include和-exclude还支持数组,能让你获取你的家目录下所有的图片文件。
Dir $home -recurse -include *.bmp,*.png,*.jpg, *.gif
做到一点即可:不要混淆了-filter 和 -include。选择这两个参数中的其中一个:具体为当你的过滤条件没有正则表达式时,使用-filter,可以显著提高效率。
 
注意:你不能使用filters在Dir中,列出确定大小的文件列表。因为Dir的限制条件只在文件和目录的名称级别。如果你想使用其它标准来过滤文件,可以尝试第五节中讲到的Where-Object。
 
下面的例子会获取你家目录下比较大的文件,指定文件至少要100MB大小。
Dir $home -recurse | Where-Object { $_.length -gt 100MB }
如果你想知道Dir返回了多少个文件项,Dir会将结果保存为一个数组,你可以通过数组的的Count属性来读取。下面的命令会告诉你你的家目录下有多少图片文件(这个操作可能会比较耗时)。
$dir=Dir $home -recurse -include *.bmp,*.png,*.jpg, *.gif
$dir.Count
 
获取文件和目录的内容
你可以访问单个文件的属性,如果它们的属性支持更改,也可以更改。
Get-Item是访问单个文件的另外一个途径, 下面的3条命令都会返回同样的结果:你指定的文件的文件对象。
$file = Dir c:\autoexec.bat
$file = Get-Childitem c:\autoexec.bat
$file = Get-Item c:\autoexec.bat
但是在访问目录而不是文件时,Get-Childitem 和 Get-Item表现迥异。
# Dir 或者 Get-Childitem 获取 一个目录下的内容:
$directory = Dir .\powershell\
$directory = Dir .\powershell\
$directory
# Get-Item 获取的是目录对象本身:
$directory = Get-Item .\powershell\
$directory
 
向命令,函数和文件脚本传递文件
因为Dir的结果中返回的是独立的文件或目录对象,Dir可以将这些对象直接交付给其它命令或者你自己定义的函数与脚本。这也使得Dir成为了一个非常重要的的选择命令。使用它你可以非常方便地在一个驱动盘下甚至多个驱动盘下递归查找特定类型的所有文件。
要做到这点,在管道中使用Where-Object来处理Dir返回的结果,然后再使用ForEach-Object,或者你自定义的管道过滤。
 
小知识:你还可以将多个Dir 命令执行的结果结合起来。在下面的例子中,两个分开的Dir命令,产生两个分开的文件列表。然后PowerShell将它们结合起来发送给管道进行深度处理。这个例子获取Windows目录和安装程序目录下的所有的dll文件,然后返回这些dll文件的名称,版本,和描述:
$list1 = Dir $env:windir\system32\*.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命令获取当前工作的目录。
如果你想导航到文件系统的另外一个位置,可以使用Set-Location或者它的别名Cd:
# 进入父目录 (相对路径):
Cd ..
# 进入当前盘的根目录 (相对路径):
Cd \
# 进入指定目录 (绝对路径):
Cd c:\windows
# 从环境变量中获取系统目录 (绝对路径):
Cd $env:windir
# 从普通变量中获取目录 (绝对路径):
Cd $home
 
相对路径和绝对路径
路径的指定可以是相对路径,也可以是绝对路径。在上面的最后一个例子中,兼而有之这两种路径。相对路径依赖你当前的路径,比如.\test.txt文件总是指定的是当前目录中的test.txt文件,而..\test.txt指定的是父目录的test.txt文件。相对路径通常比较实用,比如你想使用的脚本库位于当前工作目录,你就可以在不引入其它目录的情况下,直接工作。而绝对路径通常具有唯一性,并且独立于你当前的目录。
用于指定相对路径的四个重要的特殊字符
字符
意义
示例
示例描述
.
当前目录
Ii .
用资源浏览器打开当前目录
..
父目录
Cd ..
切换到父目录
\
驱动器根目录
Cd \
切换到驱动器的顶级根目录
~
家目录
Cd ~
切换到PowerShell初始化的目录
 
相对路径转换成绝对路径
当你使用相对路径时,PowerShell必须将这些相对转换成绝对路径。在你使用相对路径执行一个文件或者一条命令时,该转换会自动发生。你也可以自己使用Resolve-Path命令来处理。
然而,Resolve-Path命令只有在文件确实存在时,才会有效。如果你的当前文件夹中没有一个名为a.png的是,Resolve-Path名讳报错。
 
如果你指定的路径中包含了通配符,Resolve-Path还可以返回多个结果。下面的命令执行后,会获取PowerShell家目录下面的所有的ps1xml文件的名称。
像Dir一样,Resolve-Path可以在下行函数中扮演选择过滤器的的角色。下面的例子会演示在记事本中打开一个文件进行处理。命令调用记事本程序通过Resolve-Path打开这个文件。
notepad.exe (Resolve-Path  $pshome\types.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总是会返回到你的家目录,Push-Location 和 Pop-Location支持堆栈参数。这使得你可以创建很多堆栈,比如一个任务,一个堆栈。Push-Location -stack job1会把当前目录保存到job1堆栈中,而不是标准堆栈中。当然在你想重新回到这个位置时,也需要在Pop-Location中指定这个参数-stack job1。
 
查找特殊的目录
Windows使用了很多特殊的目录,根据系统的安装,可能稍有不同。一些非常重要的目录的路径同时也保存在Windows环境变量中,这样PowerShell 可以非常方便和清晰的访问它们。你也可以使用.NET framework中的Environment类去访问其它特殊目录。
存储在环境变量中的Windows特殊目录
特殊目录
描述
示例
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中找到。你可以使用下面一行脚本查看它的内容。
[System.Environment+SpecialFolder] | 
Get-Member -static -memberType Property | select -ExpandProperty Name 
如果你想预览所有GetFolderPath()支持的目录内容,可以使用下面的例子:
[System.Environment+SpecialFolder] |
Get-Member -static -memberType Property |
ForEach-Object { "{0,-25}= {1}" -f $_.name, [Environment]::GetFolderPath($_.Name) 
}
 
构造路径
路径名称由文本构成,能让你随心所欲地构造他们。你也应当看到了上面例子中构造用户桌面快捷方式的过程了:
path = [Environment]::GetFolderPath("Desktop") + "\file.txt"
$path
C:\Users\mosser\Desktop\file.txt
一定要确保你的路径中的反斜杠个数正确。这也就是为什么前面的例子中在file.txt前面使用了一个反斜杠。还有一个更可靠的方式,就是使用命令 Join-Path方法,或者.NET中的Path静态类。
 
Path类还包含了许多用来合并或者获取目录特定信息的额外方法。你只需要在下面表格中列出的方法中前加[System.IO.Path]::,比如:
[System.IO.Path]::ChangeExtension("test.txt", "ps1")
 
构造路径的方法
方法
描述
示例
ChangeExtension()
更改文件的扩展名
ChangeExtension(“test.txt”, “ps1”)
Combine()
拼接路径字符串; 对应Join-Path
Combine(“C:\test”, “test.txt”)
GetDirectoryName()
返回目录对象:对应Split-Path -parent
GetDirectoryName(“c:\test\file.txt”)
GetExtension()
返回文件扩展名
GetExtension(“c:\test\file.txt”)
GetFileName()
返回文件名:对应Split-Path -leaf
GetFileName(“c:\test\file.txt”)
GetFileNameWithoutExtension()
返回不带扩展名的文件名
GetFileNameWithoutExtension(“c:\test\file.txt”)
GetFullPath()
返回绝对路径
GetFullPath(“.\test.txt”)
GetInvalidFileNameChars()
返回所有不允许出现在文件名中字符
GetInvalidFileNameChars()
GetInvalidPathChars()
返回所有不允许出现在路径中的字符
GetInvalidPathChars()
GetPathRoot()
返回根目录:对应Split-Path -qualifier
GetPathRoot(“c:\test\file.txt”)
GetRandomFileName()
返回一个随机的文件名
GetRandomFileName()
GetTempFileName()
在临时目录中返回一个临时文件名
GetTempFileName()
GetTempPath()
返回临时文件目录
GetTempPath()
HasExtension()
如果路径中包含了扩展名,则返回True
HasExtension(“c:\test\file.txt”)
IsPathRooted()
如果是绝对路径,返回为True; Split-Path -isAbsolute
IsPathRooted(“c:\test\file.txt
 
 

使用目录和文件工作

Get-ChildItem 和 Get-Item 命令可以获取已经存在的文件和目录。你也可以创建自己的文件和目录,重命名它们,给它们填充内容,复制它们,移动它们,当然也可以删除它们。
 

1. 创建新目录

创建一个新目录最方便的方式是使用MD函数,它内部调用的是New-Item命令,指定参数–type的值为“Directory”
注意:你也可以一次性创建多层子目录,如果你指定的目录不存在,PowerShell会自动创建这些目录:
md test\subdirectory\somethingelse
只要test和Subdirectory目录都不存在,就会创建三个子目录。
 

2. 创建新文件

可能之前你已经使用过New-Item来创建过文件,但是它们完全是空的:
文件通常会在你保存数据时,自动被创建。因为空文件一般没多大用处。此时重定向和Out-File,Set-Content这两个命令可以帮助你:
Dir > info1.txt 
Dir | Out-File info2.txt
Dir | Set-Content info3.txt
Set-Content info4.txt (Get-Date) 
事实证明在操作上重定向Out-File非常的类似:当PowerShell转换管道结果时,文件的内容就像它在控制台上面输出的一样。Set-Content呢,稍微有所不同。它在文件中只列出目录中文件的名称列表,因为在你使用Set-Content时,PowerShell不会自动将对象转换成文本输入。相反,Set-Content会从对象中抽出一个标准属性。上面的情况下,这个属性就是Name了。
通常,你可以将任何文本写入一个文本文件。最后一行演示的是将一个日期对象写入到文件中。比如你手动使用ConvertTo-HTML将管道结果转换后,Out-FileSet-Content会殊途同归。
Dir | ConvertTo-Html | Out-File report1.htm 
Dir | ConvertTo-Html | Set-Content report2.htm
如果你想决定对象的那个属性应当显示在HTML页面中,可以使用第5章中提到的Select-Object 在对象转换成HTML前过滤属性。
Dir | Select-Object name, length, LastWriteTime |
ConvertTo-HTML | Out-File report.htm
在重定向的过程中,控制台的编码会自动指定特殊字符在文本中应当如何显示。你也可以在使用Out-File命令时,使用-encoding参数来指定。
如果你想将结果导出为逗号分割符列表,可以使用Export-CSV代替Out-File
你可以使用双重定向Add-Content向一个文本文件中追加信息。
这个结果让小伙伴们惊呆了:双箭头重定向可以工作,但是文本中显示的字符有间隔。重定向操作符通常使用的是控制台的字符集,如果你的文本中碰巧同时包含了ANSI和Unicode字符集,可能会引起意外的结果。相反,使用Set-ContentAdd-ContentOut-File这几条命令,而不使用重定向,可以有效地规避前面的风险。这三条命令都支持-encoding参数,你可以用它来选择字符集。
 

3. 创建新驱动器

你可能会惊讶,PowerShell允许你创建新的驱动器。并且不会限制你只创建基于网络的驱动器。你还可以使用驱动器作为你的文件系统中重要目录,甚至你自定义的文件文件系统的一个方便的快捷方式。
使用New-PSDrive命令来创建一个新的驱动器。可以像下面那样创建一个网络驱动器(前提是要先创建一个文件共享)。
在工作目录中创建一个快捷方式也非常方便。下面的命令行会创建一个名为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
 

4. 读取文本文件的内容

使用Get-Content可以获取文本文件的内容:
Get-Content $env:windir\windowsupdate.log
如果你知道文件的绝对路径,还可以使用变量符号这个快捷方式读取文本内容:
${c:\windows\windowsupdate.log}
通常,这个符号不是很实用,因为在括号中不允许适用任何变量。而大多数情况下绝对路径不会适用所有机器的操作系统。
Get-Content 逐行读取文本的内容,然后把文本的每一行传递给管道。因此,在你想读取一个长文件的前10行,应当适用Select-Object
Get-Content $env:windir\windowsupdate.log | Select-Object -first 10
使用Select-String可以过滤出文本文件中的信息。下面的命令行会从windowsupdate.log文件中过滤出包含”added update”短语的行。
Get-Content $env:windir\windowsupdate.log | Select-String "Added update"
 

5. 处理逗号分隔的列表

在PowerShell中处理逗号分隔的列表文件中的信息时你须要使用Import-Csv文件。为了测试,先创建一个逗号分隔的文本文件。
然后就可以使用Import-Csv输入列表文件了,
如你所见,Import-Csv理解逗号文件的格式,并且可以逐列显示数据。所以在解析逗号分割的文本文件时,你可以节省下很多工作量:Import-Csv会替你完成。第一行被解析成列的标题。然后你就可以将非常方便地将逗号分隔的值作为输入,比如创建用户账号。
高级主题:除了使用ForEach-Object循环你还可以在括号中使用脚本块。对于每一个管道内部的管道对象,脚本块都会被执行。在下面的例子中,逗号分割文件中的每一个用户名都会通过echo的参数-InputObject返回并输出。
Import-Csv user.txt | echo -InputObject {$_.Username }
 

6. 移动和复制文件和目录

Move-Item 和 Copy-Item用来执行移动和拷贝操作。它们也支持通配符。比如下面的脚本会将你家目录下的的所有PowerShell脚本文件复制到桌面上:
Copy-Item $home\*.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")
此时,你的桌面又恢复了往日的整洁,也把脚本安全的保存到桌面了。
 

7. 重命名文件和目录

使用Rename-Item你可以给文件或者目录换个名字。但是这样做时要格外小心,因为如果把某些系统文件给重命名了,可能会导致系统瘫痪。甚至你只是更改了某些文件的扩展名,也会导致它们不能正常打开或者显示它们的一些属性。
Set-Content testfile.txt "Hello,this,is,an,enumeration"
# 在默认编辑器中打开文件:
.\testfile.txt
# 在Excel中打开文件:
Rename-Item testfile.txt testfile.csv
.\testfile.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 }
由于-whatIf参数的缘故,一开始语句只会表明可能会执行重命名操作。
 
整理文件名
数据集往往随着时间的增长而增长。如果你想整理一个目录,你可以给定所有的文件一个统一的名称和序号。你可以从文件的某些具体的属性中合成文件名。还记得上面在桌面上为PowerShell脚本创建的那个子目录吗?让我们对它里面的PowerShell脚本以数字序号重命名吧。
Dir $directory\*.ps1 | ForEach-Object {$x=0} {
Rename-Item $_ ("Script " + $x + ".ps1"); $x++ } {"Finished!"}
Dir $directory\*.ps1
 

8. 删除文件和目录

使用Remove-Item和别名Del可以删除文件和目录,它会不可恢复的删除文件和目录。如果一个文件属于只读文件,你需要指定参数-force :
 
删除目录内容
如果你只想删除某个目录下的内容而保留目录本身,可以使用通配符。比如下面的脚本行会删除Recent目录下的内容,对应于启动菜单中的“My Recent Documents”。因为删除文件夹是一件掉以轻心就会产生严重后果的事情,所有你可以使用-whatIf参数模拟一下删除过程,看看可能会发生什么。
$recents = [Environment]::GetFolderPath("Recent")
del $recents\*.* -whatIf
如果你已经确认你的命令操作无误,将上面语句中的-whatif去掉即可删除这些文件。另一方面,如果你仍然不是很确定,可以使用-confirm,它会在每次删除操作执行前向你确认。
 
删除目录和它的内容
如果一个目录被删除了,它里面所有的内容都会丢失。在你尝试去删除一个文件夹连同它的内容时,PowerShell都会请求你的批准。这样是为了防止你无意间销毁大量数据。只有空目录才不需要请求确认信息。
 
 

管理访问权限

对于NTFS驱动器来说,访问权限决定着那个用户可以访问文件和目录。对于每一个文件和文件夹,所谓的安全描述符(SD)规定了安全数据。安全描述符决定安全设置是否只对当前目录有效,或者它可以被传递给其它文件和目录。真正的访问权限是在访问控制列表(ACL)中。每一个访问权限的访问控制项(ACE)也在ACL中。
注意:文件和目录访问权限相当于一个复杂的电子锁。如果使用得当,你可以把它变成一个有力的安全系统。然而,如果使用不当,你可能很容易把自己锁在外面,失去了访问重要数据的权限,或者破坏了Windows操作系统(当你无意间禁止了访问关键系统目录的权限后)。作为文件和目录的所有者,你总是有更正权限的选项;作为一个管理员,你也总能取得文件和目录的拥有权。但这是不得已的后门,你不能依赖它:你应当在你能意识到后果的情况下更改权限。最好一开始使用测试文件和目录做实验。
PowerShell使用Get-Acl 命令 Set-Acl 来管理权限。此外,类似cacls这样的传统命令也可以在PowerShell的控制台上面使用。通常他们更改起来访问权限会比PowerShell命令更快。尤其在你处理非常多的文件和目录时。由于Windows Vista的发布,cacls一直被视为过时。如果可能的化,你可以使用它的继任者icacls
 

1. 检查有效的安全设置

文件和目录的有效安全设置在访问控制列表中,使用Get-Acl时,会获取列表中的内容。因此如果你想找出谁能够访问某些文件或者目录,可以这样处理:
 
确认文件所有者的身份
文件和目录的所有者还有一些特殊的权限。比如文件的所有者总是能够访问文件。你可以通过Owner属性,来获取所有者名称。
 
列出访问权限
实际上访问权限就是——谁可以做什么,下面输出访问属性:
在上面表格的IdentityReference列,告诉你谁有特殊的权限。FileSystemRights列告诉你权限的类型。AccessControlType列格外重要,如果它显示“拒绝”而不是“允许”,你懂的,它会限制用户访问。
 

2. 创建新的权限

Get-Acl执行后返回的对象,包含若干方法可以用来更新权限和设定所有权。如果你只想设定自己的权限,都没必要去安全描述符世界深究。往往,读取一个已经存在的文件安全描述符,把它传递给另一个文件,或者按照特殊SDDL语言文字的形式指定安全信息就够了。
技巧:下面的例子会让你认识一些日常步骤。注意两点即可:别忘了cacls这个可靠的工具,因为使用它会比PowerShell命令更高效。此外,Get-ACL和Set-ACL不仅仅应用于文件层面,还可以用于其它有访问控制的安全描述符的任何地方,比如Windows注册表。
 
克隆权限
在一个初级的案例中,你可能都不会创建任何新的权限,只会从一个已经存在的文件或者目录的访问控制列表中克隆一个权限,然后把它转让给其它文件。优点是可以使用图形用户界面来设置那些通常比较复杂的权限。
注意:因为手动调整安全设置是一项专业,各个Windows系统不通用(像Windows XP Home就没有这个选项)的工作。尽管如此,你却可以使用PowerShell在不同的Windows版本中设置权限。
开始之前,先创建两个目录作为测试:
md Prototype | out-null
md Protected | out-null
现在,打开资源管理器,设置Prototype目录的安全设置。
explorer .
在资源管理器中,右击Prototype目录,选择属性,然后点击安全选项卡,点击编辑(win7和win8中)。通过添加其他用户来更改测试目录的安全设置。在下面的对话框中给新用户设置权限。
注意:你也可以通过勾选拒绝复选框来拒绝用户的权限。这样做时,可要留心了。因为限制权限总是有高优先级。比如,你给了自己完全控制的权限,但是拒绝了“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目录的安全信息。
然后把这个SDDL文本保存到第二个脚本中,可将该安全设置赋给任意目录。
# 创建新目录
Md Protected
# 在 SDDL中的是安全描述符 (一行):
$sddl = "O:S-1-5-21-1326624934-3632289205-3429467323-1001G:S-1-5-21-1326624934-3632289205-3429467323-513D:P(A;OICI;FA;;;S-1-5-21-1326624934-3632289205-3429467323-1001)"
# 获取目录的安全描述:
$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对象显示这些规范。你可以使用下面的机器列出访问权限允许的值:
[System.Enum]::GetNames([System.Security.AccessControl.FileSystemRights])
如果你想设置权限时,实际上得结合上面列表中列出的相关值,比如:
因为这里你没有指定二进制计算符-bor,它的结果是可读的文本。而此时需要位掩码来工作,所以把它转换成Integer整形数据类型。你可以像这样随时得出设置的相关值。
这样做的意义在于你现在可以测试其它.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,你也可以在脚本中手动更改用户名,看看指定的用户名是否存在。
一个NTAccount对象描述了一个权限可以分配的安全主体。在实践中,它是用户和组。NTAccount对象可以使用Translate()来输出它包含的与主体对应的SID。而这只会在指定的账号确实存在的情况下有效。否则,你会得到一个错误。因此你也可以使用Translate()来验证一个账号的存在性。
通过Translate()获取的SID非常有用。如果你仔细看,你会发现管理员组的SID和你自己当前账号的SID完全不同:
管理员组的SID不但很短,而且是唯一的。为了整合这个账号,Windows使用了所谓的众所周知的SID,它在所有的Windows系统中都是相同的。这一点很重要,因为德文系统中运行上面的脚本会失败。在本地化的德文系统上,因为Administrators组叫做”Administratoren”,”Everyone”组叫做”Jeder”。但是这些账号的SID是相同的。知道了这些组的SID号,你就可以使用它们代替那些本地化的名称了。下面是怎样将SID转换成用户账号的名称:
这是怎样让你的脚本能够非常完美地在国际本地化机器上运行:
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
 

 

posted @ 2021-06-26 21:33  Ulysses~  阅读(368)  评论(0编辑  收藏  举报