• PowerShell之 文件系统:使用目录和文件工作

 

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

创建新目录

创建一个新目录最方便的方式是使用MD函数,它内部调用的是New-Item命令,指定参数–type的值为“Directory”:

# "md"是一个内置的函数用来创建新目录
PS D:\\> md Test1

    目录: D:\\

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----         2014/1/23     18:13            Test1

# "New-Item",也可以做这些,但是得多花点功夫
PS D:\\> New-Item Test2 -type Directory

    目录: D:\\

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----         2014/1/23     18:13            Test2

注意:你也可以一次性创建多层子目录,如果你指定的目录不存在,PowerShell会自动创建这些目录:

1

md test\\subdirectory\\somethingelse

只要test和Subdirectory目录都不存在,就会创建三个子目录。

创建新文件

可能之前你已经使用过New-Item来创建过文件,但是它们完全是空的:

PS> New-Item "new file.txt" -type File

    目录: D:\\

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---         2014/1/23     19:14          0 new file.txt

文件通常会在你保存数据时,自动被创建。因为空文件一般没多大用处。此时重定向和Out-File,Set-Content这两个命令可以帮助你:

Dir > info1.txt
.\\info1.txt
Dir | Out-File info2.txt
.\\info2.txt
Dir | Set-Content info3.txt
.\\info3.txt
Set-Content info4.txt (Get-Date)
.\\info4.txt

事实证明在操作上重定向Out-File非常的类似:当PowerShell转换管道结果时,文件的内容就像它在控制台上面输出的一样。Set-Content呢,稍微有所不同。它在文件中只列出目录中文件的名称列表,因为在你使用Set-Content时,PowerShell不会自动将对象转换成文本输入。相反,Set-Content会从对象中抽出一个标准属性。上面的情况下,这个属性就是Name了。

通常,你可以将任何文本写入一个文本文件。最后一行演示的是将一个日期对象写入到文件中。比如你手动使用ConvertTo-HTML将管道结果转换后,Out-FileSet-Content会殊途同归。

1

2

3

4

Dir | ConvertTo-HTML | Out-File report1.htm

.\\report1.htm

Dir | ConvertTo-HTML | Set-Content report2.htm

.\\report2.htm

如果你想决定对象的那个属性应当显示在HTML页面中,可以使用第5章中提到的Select-Object 在对象转换成HTML前过滤属性

1

2

3

Dir | Select-Object name, length, LastWriteTime |

ConvertTo-HTML | Out-File report.htm

.\\report.htm

在重定向的过程中,控制台的编码会自动指定特殊字符在文本中应当如何显示。你也可以在使用Out-File命令时,使用-encoding参数来指定。

如果你想将结果导出为逗号分割符列表,可以使用Export-CSV代替Out-File

你可以使用双重定向Add-Content向一个文本文件中追加信息。

Set-Content info.txt "First line"
"Second line" >> info.txt
Add-Content info.txt "Third line"
Get-Content info.txt
First Line
S e c o n d L i n e
Third line

这个结果让小伙伴们惊呆了:双箭头重定向可以工作,但是文本中显示的字符有间隔。重定向操作符通常使用的是控制台的字符集,如果你的文本中碰巧同时包含了ANSI和Unicode字符集,可能会引起意外的结果。相反,使用Set-ContentAdd-ContentOut-File这几条命令,而不使用重定向,可以有效地规避前面的风险。这三条命令都支持-encoding参数,你可以用它来选择字符集。

创建新驱动器

你可能会惊讶,PowerShell允许你创建新的驱动器。并且不会限制你只创建基于网络的驱动器。你还可以使用驱动器作为你的文件系统中重要目录,甚至你自定义的文件文件系统的一个方便的快捷方式。

使用New-PSDrive命令来创建一个新的驱动器。可以像下面那样创建一个网络驱动器。

PS> New-PSDrive -name network -psProvider FileSystem -root \\\\127.0.0.1\\share

Name           Used (GB)     Free (GB) Provider      Root                                               CurrentLocation
----           ---------     --------- --------      ----                                               ---------------
network                                FileSystem    \\\\127.0.0.1\\share

PS> dir network:

    目录: \\\\127.0.0.1\\share

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目录:“我的文档”。

1

2

3

4

New-PSDrive desktop FileSystem `

([Environment]::GetFolderPath("Desktop")) | out-null

New-PSDrive docs FileSystem `

([Environment]::GetFolderPath("MyDocuments")) | out-null

然后你想更改当前目录为桌面时,只须输入:

1

Cd desktop:

使用Remove-PSDrive来删除你创建的驱动器。如果该驱动器正在使用则不能删除。注意在使用New-PSDriveRemove-PSDrive创建或删除驱动器时,指定的字母不能包含冒号,但是在使用驱动器工作时必须指定冒号。

1

Remove-PSDrive desktop

读取文本文件的内容

使用Get-Content可以获取文本文件的内容:

1

Get-Content $env:windir\\windowsupdate.log

如果你知道文件的绝对路径,还可以使用变量符号这个快捷方式读取文本内容:

1

${c:\\windows\\windowsupdate.log}

通常,这个符号不是很实用,因为在括号中不允许适用任何变量。而大多数情况下绝对路径不会适用所有机器的操作系统。

Get-Content 逐行读取文本的内容,然后把文本的每一行传递给管道。因此,在你想读取一个长文件的前10行,应当适用Select-Object:

1

Get-Content $env:windir\\windowsupdate.log | Select-Object -first 10

使用Select-String可以过滤出文本文件中的信息。下面的命令行会从windowsupdate.log文件中过滤出包含”added update”短语的行。

1

Get-Content $env:windir\\windowsupdate.log | Select-String "Added update"

处理逗号分隔的列表

在PowerShell中处理逗号分隔的列表文件中的信息时你须要使用Import-Csv文件。为了测试,先创建一个逗号分隔的文本文件。

Set-Content user.txt "Username,Function,Passwordage"
Add-Content user.txt "Tobias,Normal,10"
Add-Content user.txt "Martina,Normal,15"
Add-Content user.txt "Cofi,Administrator,-1"
Get-Content user.txt
Username,Function,Passwordage
Tobias,Normal,10
Martina,Normal,15
Cofi,Administrator,-1

然后就可以使用Import-Csv输入列表文件了,

PS> Import-Csv user.txt

Username Function      Passwordage
-------- --------      -----------
Tobias   Normal        10
Martina  Normal        15
Cofi     Administrator -1

如你所见,Import-Csv理解逗号文件的格式,并且可以逐列显示数据。所以在解析逗号分割的文本文件时,你可以节省下很多工作量:Import-Csv会替你完成。第一行被解析成列的标题。然后你就可以将非常方便地将逗号分隔的值作为输入,比如创建用户账号。

Import-Csv user.txt | ForEach-Object { $_.Username }
Tobias
Martina
Cofi

高级主题:除了使用ForEach-Object循环你还可以在括号中使用脚本块。对于每一个管道内部的管道对象,脚本块都会被执行。在下面的例子中,逗号分割文件中的每一个用户名都会通过echo的参数-InputObject返回并输出。

Import-Csv user.txt | echo -InputObject {$_.Username }

解析文本内容和提取文本信息

经常会碰到的一个任务就是解析原始数据,比如日志文件,从所有的数据中获取结构化的目标信息。比如日志文件:windowsupdate.log 它记录了windows更新的细节信息(在之前的例子中我们已经多次用到过这个小白鼠)。该文件还有大量数据,以至于乍一看没什么可读性。初步分析表明该文件是逐行存储的信息,并且每行的信息片段是以Tab字符分割的。

正则表达式为描述这类文件格式提供了最方便的方式,之前在第13章已经提到过。你可以按照下面的例子来使用正则表达式适当地描述文件indowsupdate.log的内容。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

# 文本模式包含了6个Tab字符分割的数组

$pattern = "(.*)\\t(.*)\\t(.*)\\t(.*)\\t(.*)\\t(.*)"

# 输入日志

$text = Get-Content $env:windir\\windowsupdate.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

这种情况下,推荐给每一个子表达式取一个名字,这样可以在后面通过该名字访问。

1

2

3

4

5

6

7

8

9

10

11

12

13

# 这次子表达式拥有一个名称:

$pattern = "(?.*)\\t(?.*)\\t(?.*)" + "\\t(?.*)\\t(?.*)\\t(?.*)"

# 输入日志:

$text = Get-Content $env:windir\\windowsupdate.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:windir\\windowsupdate.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-ContentForEach-Object更快速你只需要记住正则表达式“.*”代表任意数量的任意字符。

Switch -regex -file $env:windir\\wu1.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:windir\\wu1.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个字节。

1

2

3

4

5

6

7

8

9

10

11

12

13

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:windir\\explorer.exe"
4d5a 9000 'M Z . .'

Explorer的前四个字节为4d, 5a, 90, 和 00或者已经列出的文本MZ。这是Microsoft DOS的开发者之一Mark Zbikowski的简称。所以,标记MZ就代表了可执行的程序。这个标记和图片文件的标记不同:

PS> Get-MagicNumber "$env:windir\\Web\\Wallpaper\\Scenes\\*"
ffd8 ffe0 '   à'
ffd8 ffe0 '   à'
ffd8 ffe0 '   à'
ffd8 ffe0 '   à'
ffd8 ffe0 '   à'
ffd8 ffe0 '   à'

如你所见,Get-Content也可以读取二进制文件,一次只读一个字节。参数-readCount指定每一步读取多少个字节。-totalCount指定总共要读取的字节数,一旦给它赋值为-1,它会从头到尾读取所有文件内容。你可以通过将数据输出为十六进制来预览可执行文件。因为纯二进制文本不易阅读。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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> Get-HexDump $env:windir\\explorer.exe -width 15 -bytes 150
 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 MZ...........
 00 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 ...............
 e0 00 00 00 0e 1f ba 0e 00 b4 09 cd 21 b8 01 à............
 4c cd 21 54 68 69 73 20 70 72 6f 67 72 61 6d L.This.program
 20 63 61 6e 6e 6f 74 20 62 65 20 72 75 6e 20 .cannot.be.run.
 69 6e 20 44 4f 53 20 6d 6f 64 65 2e 0d 0d 0a in.DOS.mode....
 24 00 00 00 00 00 00 00 93 83 28 37 d7 e2 46 ...........7.F
 64 d7 e2 46 64 d7 e2 46 64 de 9a c2 64 9d e2 d.Fd.Fd.d.

移动和复制文件和目录

Move-Item 和 Copy-Item用来执行移动和拷贝操作。它们也支持通配符。比如下面的脚本会将你家目录下的的所有PowerShell脚本文件复制到桌面上:

1

Copy-Item $home\\*.ps1 ([Environment]::GetFolderPath("Desktop"))

但是,只有在家目录当下的脚本会被复制。幸亏Copy-Item还有一个参数-recurse,这个参数的效果类似Dir中的效果。如果你的初始化目录不包含任何目录,它也不会工作。

1

Copy-Item -recurse $home\\*.ps1 ([Environment]::GetFolderPath("Desktop"))

使用Dir也可以复制所有PowerShell脚本到你的桌面,让我们先给你找出这些脚本,然后将结果传递给Copy-Item:

1

2

Dir -filter *.ps1 -recurse | ForEach-Object {

Copy-Item $_.FullName ([Environment]::GetFolderPath("Desktop")) }

小技巧:你可能被诱惑去缩减脚本行,因为文件对象整合了一个CopyTo()方法。

1

2

Dir -filter *.ps1 -recurse | ForEach-Object {

$_.CopyTo([Environment]::GetFolderPath("Desktop")) }

但是结果可能会出错,因为CopyTo()是一个低级的函数。它需要文件的目标路径也被复制。因为你只是想复制所有文件到桌面,你已经指定了目标路径的目录。CopyTo()会尝试将文件复制这个精确的字符串路径(桌面)下,但是肯定不会得逞,因为桌面是一个已经存在的目录了。相反的Copy-Item就聪明多了:如果目标路径是一个目录,它就会把文件复制到这个目录下。

此时,你的桌面上可能已经堆满了PowerShell脚本,最好的方式是将它们保存到桌面的一个子目录中。你需要在桌面上创建一个新目录,然后从桌面到这个子目录中移动所有的脚本。

1

2

3

$desktop = [Environment]::GetFolderPath("Desktop")

md ($desktop + "\\PS Scripts")

Move-Item ($desktop + "\\*.ps1") ($desktop + "\\PS Scripts")

此时,你的桌面又恢复了往日的整洁,也把脚本安全的保存到桌面了。

重命名文件和目录

使用Rename-Item你可以给文件或者目录换个名字。但是这样做时要格外小心,因为如果把某些系统文件给重命名了,可能会导致系统瘫痪。甚至你只是更改了某些文件的扩展名,也会导致它们不能正常打开或者显示它们的一些属性。

1

2

3

4

5

6

Set-Content testfile.txt "Hello,this,is,an,enumeration"

# 在默认编辑器中打开文件:

.\\testfile.txt

# 在Excel中打开文件:

Rename-Item testfile.txt testfile.csv

.\\testfile.csv

批量重命名

因为Rename-Item可以在管道中的语句块中使用,这就给一些复杂的任务提供了令人惊讶的方便的解决方案。比如,你想将一个目录的名称和它的子目录的名称,包括目录下的文件的名称中所有的“x86”词语移除掉。下面的命令就够了:

1

2

Dir | ForEach-Object {

Rename-Item $_.Name $_.Name.replace("-x86", "") }

然而,上面的命令会实际上会尝试重命名所有的文件和目录,即使你找的这个词语在文件名中不存在。产生错误并且非常耗时。为了大大提高速度,可是使用Where-Object先对文件名进行过滤,然后对符合条件的文件进行重命名,可以将速度增长50倍:(荔非苔注:为什么是50倍呢?我不知道。)

1

2

Dir | Where-Object { $_.Name -contains "-x86" } | ForEach-Object {

Rename-Item $_.Name $_.Name.replace("-x86", "") }

更改文件扩展名

如果你想更改文件的扩展名,首先需要意识到后果:文件随后会识别为其它文件类型,而且可能被错误的应用程序打开,甚至不能被任何应用程序打开。下面的命令会把当前文件夹下的所有的PowerShell脚本的后缀名从“.ps1”改为“.bak”。

1

2

3

4

5

6

Dir *.ps1 | ForEach-Object { Rename-Item $_.Name `

([System.IO.Path]::GetFileNameWithoutExtension($_.FullName) + `

".bak") -whatIf }

What if: Performing operation "Rename file" on Target

"Element: C:\\Users\\Tobias Weltner\\tabexpansion.ps1

Destination: C:\\Users\\Tobias Weltner\\tabexpansion.bak".

由于-whatIf参数的缘故,一开始语句只会表明可能会执行重命名操作。

整理文件名

数据集往往随着时间的增长而增长。如果你想整理一个目录,你可以给定所有的文件一个统一的名称和序号。你可以从文件的某些具体的属性中合成文件名。还记得上面在桌面上为PowerShell脚本创建的那个子目录吗?让我们对它里面的PowerShell脚本以数字序号重命名吧。

1

2

3

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:\\Users\\Tobias Weltner\\testfile.txt: Not enough permission to perform operation.
At line:1 char:4
+ del <<<< testfile.txt
del testfile.txt -force
Table
删除目录内容

如果你只想删除某个目录下的内容而保留目录本身,可以使用通配符。比如下面的脚本行会删除Recent目录下的内容,对应于启动菜单中的“My Recent Documents”。因为删除文件夹是一件掉以轻心就会产生严重后果的事情,所有你可以使用-whatIf参数模拟一下删除过程,看看可能会发生什么。

1

2

$recents = [Environment]::GetFolderPath("Recent")

del $recents\\*.* -whatIf

如果你已经确认你的命令操作无误,将上面语句中的-whatif去掉即可删除这些文件。另一方面,如果你仍然不是很确定,可以使用-confirm,它会在每次删除操作执行前向你确认。

删除目录和它的内容

如果一个目录被删除了,它里面所有的内容都会丢失。在你尝试去删除一个文件夹连同它的内容时,PowerShell都会请求你的批准。这样是为了防止你无意间销毁大量数据。只有空目录才不需要请求确认信息。

# 新建一个测试目录:
md testdirectory

Directory: Microsoft.PowerShell.Core\\FileSystem::C:\\Users\\Tobias Weltner\\Sources\\docs

Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 13.10.2007 13:31 testdirectory

# 在目录中新建一个文件
Set-Content .\\testdirectory\\testfile.txt "Hello"

# 删除目录 directory:
del testdirectory

Confirm
The item at "C:\\Users\\Tobias Weltner\\Sources\\docs\\testdirectory" 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会将这个目录连同它里面的内容删除,没有任何确认提示

 

  • 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时,会获取列表中的内容。因此如果你想找出谁能够访问某些文件或者目录,可以这样处理:

# 列出Windows目录的权限:
PS> Get-Acl $env:windir

    Directory: C:\\

Path    Owner                       Access
----    -----                       ------
Windows NT SERVICE\\TrustedInstaller CREATOR OWNER Allow  268435456...
确认文件所有者的身份

文件和目录的所有者还有一些特殊的权限。比如文件的所有者总是能够访问文件。你可以通过Owner属性,来获取所有者名称。

1

2

3

(Get-Acl $env:windir).Owner

NT SERVICE\\TrustedInstaller

列出访问权限

实际上访问权限就是——谁可以做什么,下面输出访问属性:

PS> (Get-Acl $env:windir).Access | Format-Table -wrap

   FileSystemRights   AccessControlType IdentityReference           IsInherited    InheritanceFlags    PropagationFlags
   ----------------   ----------------- -----------------           -----------    ----------------    ----------------
          268435456               Allow CREATOR OWNER                     False  ContainerInherit,          InheritOnly
                                                                                      ObjectInherit
          268435456               Allow NT AUTHORITY\\SYSTEM               False  ContainerInherit,          InheritOnly
                                                                                      ObjectInherit
Modify, Synchronize               Allow NT AUTHORITY\\SYSTEM               False                None                None
          268435456               Allow BUILTIN\\Administrat               False  ContainerInherit,          InheritOnly
                                        ors                                           ObjectInherit
Modify, Synchronize               Allow BUILTIN\\Administrat               False                None                None
                                        ors
        -1610612736               Allow BUILTIN\\Users                     False  ContainerInherit,          InheritOnly
                                                                                      ObjectInherit
   ReadAndExecute,                Allow BUILTIN\\Users                     False                None                None
        Synchronize
          268435456               Allow NT SERVICE\\TrustedI               False    ContainerInherit         InheritOnly
                                        nstaller
        FullControl               Allow NT SERVICE\\TrustedI               False                None                None
                                        nstaller

在上面表格的IdentityReference列,告诉你谁有特殊的权限。FileSystemRights列告诉你权限的类型。AccessControlType列格外重要,如果它显示“拒绝”而不是“允许”,你懂的,它会限制用户访问。

创建新的权限

Get-Acl执行后返回的对象,包含若干方法可以用来更新权限和设定所有权。如果你只想设定自己的权限,都没必要去安全描述符世界深究。往往,读取一个已经存在的文件安全描述符,把它传递给另一个文件,或者按照特殊SDDL语言文字的形式指定安全信息就够了。

技巧:下面的例子会让你认识一些日常步骤。注意两点即可:别忘了cacls这个可靠的工具,因为使用它会比PowerShell命令更高效。此外,Get-ACL和Set-ACL不仅仅应用于文件层面,还可以用于其它有访问控制的安全描述符的任何地方,比如Windows注册表(会在下一章讲解)。

克隆权限

在一个初级的案例中,你可能都不会创建任何新的权限,只会从一个已经存在的文件或者目录的访问控制列表中克隆一个权限,然后把它转让给其它文件。优点是可以使用图形用户界面来设置那些通常比较复杂的权限。

注意:因为手动调整安全设置是一项专业,各个Windows系统不通用(像Windows XP Home就没有这个选项)的工作。尽管如此,你却可以使用PowerShell在不同的Windows版本中设置权限。

开始之前,先创建两个目录作为测试:

1

2

md Prototype | out-null

md Protected | out-null

现在,打开资源管理器,设置Prototype目录的安全设置。

1

explorer .

在资源管理器中,右击Prototype目录,选择属性,然后点击安全选项卡,点击编辑(win7和win8中)。通过添加其他用户来更改测试目录的安全设置。在下面的对话框中给新用户设置权限。

注意:你也可以通过勾选拒绝复选框来拒绝用户的权限。这样做时,可要留心了。因为限制权限总是有高优先级。比如,你给了自己完全控制的权限,但是拒绝了“Everyone”这个组来访问。这样就把自己关在文件系统的外面了。因为你也属于”Everyone”这个组,同时因为限制的优先级比较高,哪怕你已经给了自己“完全控制”的权限,这个限制也作用于你。

你更改了权限后,捎带在资源管理器中看看第二个目录Protected。这个目录仍旧是默认赋予的权限。下一步,我们会把Prototype刚才设置的权限转交到Protected目录。

1

2

$acl = Get-Acl Prototype

Set-Acl Protected $acl

注意:你本身需要特殊的权限去设置上面的权限。如果你用的是Windows Vista操作系统,并且启用了UAC,使用PowerShell操作时,会出现错误,提示你没有权限。这时可以通过让控制台以管理员权限运行来获取权限。

实验做完了,现在呢,Protected和Prototype一样安全。当你在资源管理器中查看它们的安全设置时,你会发现所有的设置都是相同的。

使用SDDL设置权限

前面的例子非常简单,你所做的只是把已有目录的安全设置移交给其它目录。在你的日常工作中,你可能得具备一个你根本就不需要的Prototype目录。但是你可以通过文本格式的安全描述符来归纳安全设置。每一个安全设置都是被特殊的安全描述符描述语言(SDDL)定义的。它能让你以文本的形式读取Prototype目录的安全信息,以后无须借助Prototype目录即可使用。

让我们删掉这个测试目录Protected吧,然后在SDDL中保存Prototype目录的安全信息。

PS> Del Protected
PS> $acl = Get-Acl Prototype
PS> $sddl = $acl.Sddl
PS> $sddl
O:S-1-5-21-2146773085-903363285-719344707-1282827G:DUD:PAI(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;S-1-5-21-2146773085-903363285-719344707-1282827)(A;OICI;FR;;;S-1-5-21-2575865618-2571387221-2201921913-1000)

然后把这个SDDL文本保存到第二个脚本中,可将该安全设置赋给任意目录。

1

2

3

4

5

6

7

8

9

10

# 创建新目录

Md Protected

# 在 SDDL中的是安全描述符 (一行):

$sddl = "O:S-1-5-21-2146773085-903363285-719344707-1282827G:DUD:PAI(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;S-1-5-21-2146773085-903363285-719344707-1282827)(A;OICI;FR;;;S-1-5-21-2575865618-2571387221-2201921913-1000)"

# 获取目录的安全描述:

$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,再次创建一个新的目录,让它只有默认的访问权限。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

$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.FileSystemAcce***ule( $person,$access,$inheritance,$propagation,$type)

$acl.AddAcce***ule($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.FileSystemAcce***ule( $person,$access,$inheritance,$propagation,$type)

$acl.AddAcce***ule($rule)

# 保存权限更新:

Set-Acl Protected $acl

接下来,让我们一起看看每个访问规则是怎么定义的。每一个规则需要5个细节:

  • Person:这是该规则应当适用的人或者组。

  • Access:这里选择规则要控制的权限。

  • Inheritance:这里选择规则要应用的对象。这个规则能够,并且一般是会授予它的子对象,这样它就能自动适用于目录中的文件了。

  • Propagation:决定权限是否要传递给子对象(比如子目录和文件),通常情况下设置为None,仅仅授予权限。

  • Type:它能让你设置权限或者限制,如果限制,指定的权限会明确不予批准。

接下来问题是这些规范允许那些值?这个例子演示通过.NET对象(第六章)显示这些规范。你可以使用下面的机器列出访问权限允许的值:

[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

如果你想设置权限时,实际上得结合上面列表中列出的相关值,比如:

$access = [System.Security.AccessControl.FileSystemRights]::Read `
-bor [System.Security.AccessControl.FileSystemRights]::Write
$access

131209

结果是一个数字,读和写权限的位掩码。在上面的例子中,你可以非常简单第获取相同的结果,因为允许你指定你想要的项目,甚至把它们放在一个逗号分隔项中,紧跟在括号括起来的.NET枚举类型后面。

$access = [System.Security.AccessControl.FileSystemRights]"Read,Write"
$access
   Write, Read

[int]$access
   131209

因为这里你没有指定二进制计算符-bor,它的结果是可读的文本。而此时需要位掩码来工作,所以把它转换成Integer×××数据类型。你可以像这样随时得出设置的相关值。

[int][System.Security.AccessControl.InheritanceFlags] `
"ObjectInherit,ContainerInherit"
3

这样做的意义在于你现在可以测试其它.NET枚举类型的值,把它们转换成整数。虽然不能增强你的命令的可读性,但是可以压缩脚本。因为下面的脚本行和前面例子中的脚本行可以做同一件事。

1

2

3

4

5

6

7

8

9

Del Protected

Md Protected

$acl = Get-Acl Protected

$rule = New-Object System.Security.AccessControl.FileSystemAcce***ule( "Administrator",2032127,3,0,0)

$acl.AddAcce***ule($rule)

$rule = New-Object System.Security.AccessControl.FileSystemAcce***ule( "Everyone",131241,3,0,0)

$acl.AddAcce***ule($rule)

# 保存更新的权限:

Set-Acl Protected $acl

最后,我们看看PowerShell是怎么指定特定用户的权限的。在上面的例子中,你指定了用户或者组的名称,但是权限不能识别用户名,但能识别账号的唯一SID,用户名在内部会被更改成SID,你也可以在脚本中手动更改用户名,看看指定的用户名是否存在。

$Account = [System.Security.Principal.NTAccount]"Administrators"
$SID = $Account.translate([System.Security.Principal.Securityidentifier])
$SID
BinaryLength	AccountDomainSid	Value
------------	----------------	-----
16 S-1-5-32-544

一个NTAccount对象描述了一个权限可以分配的安全主体。在实践中,它是用户和组。NTAccount对象可以使用Translate()来输出它包含的与主体对应的SID。而这只会在指定的账号确实存在的情况下有效。否则,你会得到一个错误。因此你也可以使用Translate()来验证一个账号的存在性。

通过Translate()获取的SID非常有用。如果你仔细看,你会发现管理员组的SID和你自己当前账号的SID完全不同:

([System.Security.Principal.NTAccount]"$env:userdomain\\$env:username").`
Translate([System.Security.Principal.Securityidentifier]).Value

S-1-5-21-2146773085-903363285-719344707-1282827

([System.Security.Principal.NTAccount]"Administrators").`
Translate([System.Security.Principal.Securityidentifier]).Value

S-1-5-32-544

管理员组的SID不但很短,而且是唯一的。为了整合这个账号,Windows使用了所谓的众所周知的SID,它在所有的Windows系统中都是相同的。这一点很重要,因为你德文系统中运行上面的脚本会失败。在本地化的德文系统上,因为Administrators组叫做”Administratoren”,”Everyone”组叫做”Jeder”。但是这些账号的SID是相同的。知道了这些组的SID号,你就可以使用它们代替那些本地化的名称了。下面是怎样将SID转换成用户账号的名称:

$sid = [System.Security.Principal.SecurityIdentifier]"S-1-1-0"
$sid.Translate([System.Security.Principal.NTAccount])

Value
-----
Everyone

这是怎样让你的脚本能够非常完美地在国际本地化机器上运行:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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.FileSystemAcce***ule( `

$sid,$access,3,0,0)

$acl.AddAcce***ule($rule)

# 所有用户的只读权限:

$sid = [System.Security.Principal.SecurityIdentifier]"S-1-1-0"

$access = [System.Security.AccessControl.FileSystemRights]"ReadAndExecute"

$rule = New-Object System.Security.AccessControl.FileSystemAcce***ule( `

$sid,$access,3,0,0)

$acl.AddAcce***ule($rule)

# 保存权限更新:

Set-Acl Protected $acl

参考:

http://www.pstips.net/powershell-online-tutorials/