PowerShell 技能连载 - 将 AD 用户转为哈希表

有些时候从一个指定的 AD 用户读取所有属性到一个哈希表中十分有用。通过这种方法,您可以编辑他们,并使用 Set-ADUser 和它的 -Add-Replace 参数将他们应用于另一个用户账户。

以下是将所有 AD 用户属性读到一个哈希表中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#requires -Version 3.0 -Modules ActiveDirectory

$blacklist = 'SID', 'LastLogonDate', 'SAMAccountName'

$user = Get-ADUser -Identity NAMEOFUSER -Properties *
$name = $user | Get-Member -MemberType *property | Select-Object -ExpandProperty Name

$hash = [Ordered]@{}
$name |
Sort-Object |
Where-Object {
$_ -notin $blacklist
} |
ForEach-Object {
$hash[$_] = $user.$_
}

请注意 $blacklist 的使用:这个列表可以包含任何希望排除的属性名。

PowerShell 技能连载 - 克隆文件夹结构(含 NTFS 权限) – 第二部分

在前一个技能中我们演示了 Get-FolderStructureWithPermission 如何以结构化的形式记录并创建一个包含所有嵌套文件夹的清单,包含它们各自的 NTFS 安全设置。结果可以保存到一个变量中,或用 Export-Csv 序列化后保存到磁盘中。

今天我们演示第二部分:当您拥有一个指定文件夹结构的信息之后,可以使用这个 Set-FolderStructureWithPermission。它输入一个要克隆其结构的文件夹路径,加上通过 Get-FolderStructureWithPermission 获得的结构信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#requires -RunAsAdministrator

function Set-FolderStructureWithPermission
{
param
(
[String]
[Parameter(Mandatory)]
$Path,

[Object[]]
$folderInfo
)

$folderInfo | ForEach-Object {
$relativePath = $_.Path
$sddl = $_.SDDL

$newPath = Join-Path -Path $Path -ChildPath $relativePath
$exists = Test-Path -Path $newPath
if ($exists -eq $false)
{
$null=New-Item -Path $newPath -ItemType Directory -Force
}
$sd = Get-Acl -Path $newPath
$sd.SetSecurityDescriptorSddlForm($sddl)
Set-Acl -Path $newPath -AclObject $sd
}
}

由于设置 NTFS 权限的需要,这个函数需要管理员特权才能运行。

免责声明:这里呈现的所有代码仅供学习使用。由于我们没有投入大量精力去测试它,所以没有任何保障,而且它并不是生产准备就绪的代码。您有责任对这段代码进行测试,并决定它是否完美符合您的需要。

一个典型的用例是克隆一个现有的文件夹结构:

1
2
3
# clone user profile
$infos = Get-FolderStructureWithPermission -Path $home
Set-FolderStructureWithPermission -Path c:\CloneHere -folderInfo $infos

PowerShell 技能连载 - 克隆文件夹结构(含 NTFS 权限) – 第一部分

有些时候您需要重新创建一个嵌套的文件夹结构,并且希望克隆 NTFS 权限。今天我们我们专注第一个步骤:记录一个已有的文件夹结构,包括 SDDL 格式的 NTFS 权限。

我们可以用 Get-FolderStructureWithPermission 函数实现这个任务。它输入一个已存在文件夹的路径,并返回所有子文件夹,包括 SDDL 格式的 NTFS 权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Get-FolderStructureWithPermission
{
param
(
[String]
[Parameter(Mandatory)]
$Path
)

if ((Test-Path -Path $Path -PathType Container) -eq $false)
{
throw "$Path does not exist or is no directory!"
}

Get-ChildItem -Path $Path -Recurse -Directory |
ForEach-Object {
$sd = Get-Acl -Path $_.FullName
$sddl = $sd.GetSecurityDescriptorSddlForm('all')


[PSCustomObject]@{
Path = $_.FullName.Substring($Path.Length)
SDDL = $sddl
}

}
}

您可以将结果通过管道输出到 Out-GridView,或将它保存到一个变量,或用 Export-Csv 将它写到磁盘中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PS C:\> Get-FolderStructureWithPermission -Path $home | Format-List


Path : \.dnx
SDDL : O:S-1-5-21-2012478179-265285931-690539891-1001G:S-1-5-21-2012478179-265285931-690539891-1001D:(A;OICIID;FA;;;SY)(A;OI
CIID;FA;;;BA)(A;OICIID;FA;;;S-1-5-21-2012478179-265285931-690539891-1001)

Path : \.plaster
SDDL : O:S-1-5-21-2012478179-265285931-690539891-1001G:S-1-5-21-2012478179-265285931-690539891-1001D:(A;OICIID;FA;;;SY)(A;OI
CIID;FA;;;BA)(A;OICIID;FA;;;S-1-5-21-2012478179-265285931-690539891-1001)

Path : \.vscode
SDDL : O:S-1-5-21-2012478179-265285931-690539891-1001G:S-1-5-21-2012478179-265285931-690539891-1001D:(A;OICIID;FA;;;SY)(A;OI
CIID;FA;;;BA)(A;OICIID;FA;;;S-1-5-21-2012478179-265285931-690539891-1001)

Path : \.vscode-insiders
SDDL : O:S-1-5-21-2012478179-265285931-690539891-1001G:S-1-5-21-2012478179-265285931-690539891-1001D:(A;OICIID;FA;;;SY)(A;OI
CIID;FA;;;BA)(A;OICIID;FA;;;S-1-5-21-2012478179-265285931-690539891-1001)

Path : \3D Objects
SDDL : O:S-1-5-21-2012478179-265285931-690539891-1001G:S-1-5-21-2012478179-265285931-690539891-1001D:(A;OICIID;FA;;;SY)(A;OI
CIID;FA;;;BA)(A;OICIID;FA;;;S-1-5-21-2012478179-265285931-690539891-1001)

...

免责声明:这里呈现的所有代码仅供学习使用。由于我们没有投入大量精力去测试它,所以没有任何保障,而且它并不是生产准备就绪的代码。您有责任对这段代码进行测试,并决定它是否完美符合您的需要。

PowerShell 技能连载 - 评估 Exit Code(也叫做 Error Level – 第三部分)

在 PowerShell 中运行控制台应用程序的迷你系列的第三部分中,有一个小课题:如何独立于 PowerShell 运行一个控制台应用程序,并且当它执行完成后得到通知,并且获取它的 exit code?

以下是实现方法:以下代码在一个独立(隐藏)的窗口中运行 ping.exe。PowerShell 继续运行并且可以执行任何其它操作。在这个例子中,它在 ping.exe 正忙于 ping 一个主机名的同时打出一系列“点”号。

当 exe 执行完成时,这段代码能获取进程的 ExitCode 信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$hostname = 'powershellmagazine.com'
# run the console-based application ASYNCHRONOUSLY in its own
# window (PowerShell continues) and return the
# process object (-PassThru)
# Hide the new window (you can also show it if you want)
$process = Start-Process -FilePath ping -ArgumentList "$hostname -n 4 -w 2000" -WindowStyle Hidden -PassThru

# wait for the process to complete, and meanwhile
# display some dots to indicate progress:
do
{
Write-Host '.' -NoNewline
Start-Sleep -Milliseconds 300
} until ($process.HasExited)
Write-Host

# the Error Level information is then found in ExitCode:
$IsOnline = $process.ExitCode -eq 0
$IsOnline

PowerShell 技能连载 - 评估 Exit Code(也叫做 Error Level – 第二部分)

当您直接启动一个控制台应用程序时,PowerShell 会返回它的 exit code(也叫做 Error Level),并存储在 $LASTEXITCODE 变量中。然而,如何获取通过 Start-Process 启动的控制台应用程序的 exit code 呢?

以下是方法:

1
2
3
4
5
6
7
8
$hostname = 'powershellmagazine.com'
# run the console-based application synchronously in the PowerShell window,
# and return the process object (-PassThru)
$process = Start-Process -FilePath ping -ArgumentList "$hostname -n 2 -w 2000" -Wait -NoNewWindow -PassThru

# the Error Level information is then found in ExitCode:
$IsOnline = $process.ExitCode -eq 0
$IsOnline

PowerShell 技能连载 - 评估 Exit Code(也叫做 Error Level – 第一部分)

当运行一个控制台应用程序时,它通常会返回一个数字型的 exit code。该 exit code 的含义取决于控制台应用程序,要查询应用程序才能理解 exit code 的含义。PowerShell 也会将 exit code 传递给用户。它通过 $LASTEXITCODE 体现。

以下是一个使用 ping.exe 来测试网络响应的例子:

1
2
3
4
5
6
7
8
9
$hostname = 'powershellmagazine.com'
# run console-based executable directly
# and disregard text results
$null = ping.exe $hostname -n 2 -w 2000
# instead look at the exit code delivered in
# $LASTEXITCODE. Ping.exe returns 0 if a
# response was received:
$IsOnline = $LASTEXITCODE -eq 0
$IsOnline

PowerShell 技能连载 - 弹出 CD 驱动器

以下是一个用 WMI 弹出 CD 驱动器的小函数。它首先向 WMI 请求所有的 CD 驱动器,然后使用 explorer 对象模型导航到该驱动器并调用它的 “Eject” 上下文菜单项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Eject-CD
{
$drives = Get-WmiObject Win32_Volume -Filter "DriveType=5"
if ($drives -eq $null)
{
Write-Warning "Your computer has no CD drives to eject."
return
}
$drives | ForEach-Object {
(New-Object -ComObject Shell.Application).Namespace(17).ParseName($_.Name).InvokeVerb("Eject")
}
}

Eject-CD

PowerShell 技能连载 - 检测 CSV 的分隔符

当使用 Import-Csv 导入一个 CSV 文件,需要指定一个分隔符。如果用错了,显然会导入失败。您需要事先知道 CSV 文件使用的分隔符。

以下是一个简单的实践,展示了如何判断一个给定的 CSV 文件的分隔符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Get-CsvDelimiter($Path)
{
# get the header line
$headerLine = Get-Content $Path | Select-Object -First 1

# examine header line per character
$headerline.ToCharArray() |
# find all non-alphanumeric characters
Where-Object { $_ -notlike '[a-z0-9äöüß"()]' } |
# find the one that occurs most often
Group-Object -NoElement |
Sort-Object -Descending -Property Count |
# return it
Select-Object -First 1 -ExpandProperty Name
}

以下是一个测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PS> Get-Date | Export-Csv -Path $env:temp\test.csv -NoTypeInformation

PS> Get-CsvDelimiter -Path $env:temp\test.csv
,

PS> Get-Date | Export-Csv -Path $env:temp\test.csv -NoTypeInformation -UseCulture

PS> Get-CsvDelimiter -Path $env:temp\test.csv
;

PS> Get-Date | Export-Csv -Path $env:temp\test.csv -NoTypeInformation -Delimiter '#'

PS> Get-CsvDelimiter -Path $env:temp\test.csv
#

PowerShell 技能连载 - 确认重复的 CSV 表头(第二部分)

当一个 CSV 文件包含重复的表头时,它无法被导入。在前一个技能中我们掩饰了如何检测一个 CSV 文件中重复的表头。以下是一个自动更正重复项的实践。

第一步,您需要一个包含重复表头的 CSV 文件。例如在德文系统中,您可以这样创建一个文件:

1
PS C:\> driverquery /V /FO CSV | Set-Content -Path $env:temp\test.csv -Encoding UTF8

快速打开该文件并检查它是否确实包含重复项。

1
PS C:\> notepad $env:temp\test.csv

如果没有重复项,请将某些表头重命名以制造一些重复项,并保存文件。

您现在可以用 Import-Csv 导入 CSV 文件了:

1
2
3
4
5
6
7
PS C:\>  Import-Csv -Path $env:temp\test.csv -Delimiter ','
Import-Csv : Element "Status" is present already.
In Zeile:1 Zeichen:1
+ Import-Csv -Path $env:temp\test.csv -Delimiter ','
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Import-Csv], ExtendedTypeSystemException
+ FullyQualifiedErrorId : AlreadyPresentPSMemberInfoInternalCollectionAdd,Microsoft.PowerShell.Commands.ImportCsvCommand

这是一个新的名为 Import-CsvWithoutDuplicate 的函数,可以自动处理重复的项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function Import-CsvWithDuplicate($Path, $Delimiter=',', $Encoding='UTF8')
{
# get the header line and all header items
$headerLine = Get-Content $Path | Select-Object -First 1
$headers = $headerLine.Split($Delimiter)

# check for duplicate header names, and if found, add an incremented
# number to it
$dupDict = @{}
$newHeaders = @(foreach($header in $headers)
{
$incrementor = 1
$header = $header.Trim('"')
$newheader = $header

# increment numbers until the new name is unique
while ($dupDict.ContainsKey($newheader) -eq $true)
{
$newheader = "$header$incrementor"
$incrementor++
}

$dupDict.Add($newheader, $header)

# return the new header, producing a string array
$newheader
})

# read the CSV without its own headers..
Get-Content -Path $Path -Encoding $Encoding |
Select-Object -Skip 1 |
# ..and replace headers with newly created list
ConvertFrom-CSV -Delimiter $Delimiter -Header $newHeaders
}

通过它,您可以安全地导入 CSV 文件,不会遇到重复的表头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
PS C:\> Import-CsvWithDuplicate -Path $env:temp\test.csv -Delimiter ','


Modulname : 1394ohci
Anzeigename : OHCI-konformer 1394-Hostcontroller
Beschreibung : OHCI-konformer 1394-Hostcontroller
Treibertyp : Kernel
Startmodus : Manual
Status : Stopped
Status1 : OK
Beenden annehmen : FALSE
Anhalten annehmen : FALSE
Ausgelagerter Pool (Bytes) : 4.096
Code(Bytes) : 204.800
BSS(Bytes) : 0
Linkdatum : 16.07.2016 04:21:36
Pfad : C:\WINDOWS\system32\drivers\1394ohci.sys
Init(Bytes) : 4.096

Modulname : 3ware
Anzeigename : 3ware
Beschreibung : 3ware
Treibertyp : Kernel
Startmodus : Manual
Status : Stopped
Status1 : OK
Beenden annehmen : FALSE
Anhalten annehmen : FALSE
Ausgelagerter Pool (Bytes) : 0
(...)

如您所见,这个函数自动将第二个 “Status” 实例重命名为 “Status1”。

PowerShell 技能连载 - 确认重复的 CSV 表头(第一部分)

CSV 文件只是文本文件,所以可以很容易地提取它的第一行并检查它的表头。如果您手头没有一个 CSV 文件,这行代码可以快速帮您创建一个:

1
2
3
PS C:\> Get-Process | Export-Csv -Path $env:temp\test.csv -NoTypeInformation -Encoding UTF8 -UseCulture

PS C:\>

现在您可以分析它的表头。这个简单的方法告诉您 CSV 文件中是否有重复的标题(在这个例子中显然不存在)。这段代码假设您的 CSV 文件分隔符是逗号。如果使用一个不同的分隔符,请调整用于分割的字符:

1
2
3
4
5
6
7
8
9
10
11
$headers = Get-Content $env:temp\test.csv | Select-Object -First 1
$duplicates = $headers.Split(',') | Group-Object -NoElement | Where-Object {$_.Count -ge 2}
if ($duplicates.Count -eq 0)
{
Write-Host 'You are safe!'
}
else
{
Write-Warning 'There are duplicate columns in your CSV file:'
$duplicates
}

结果如预想的:

1
2
3
You are safe!

PS C:\>

如果您好奇当遇到重复的标题时会如何失败,请试试这段代码:

1
PS C:\> driverquery /V /FO CSV | Set-Content -Path $env:temp\test.csv -Encoding UTF8

如果您在一个的文系统中运行这段代码,结果将会类似这样:

1
2
3
4
5
WARNUNG: There are  duplicate columns in your CSV file:

Count Name
----- ----
2 "Status"

显然,在本地化时,Microsoft 将 “State” 和 “Status” 两个单词都翻译成了德文的 “Status”,造成了重复的列标题。