Browse Source

feat(links)!: require -Path and -Target and continue after errors in the link cmdlets

New-NTFSHardLink and New-NTFSSymbolicLink now require -Path and -Target.
Without -Path, they failed with an index error; without -Target, they
used the current location, so New-NTFSSymbolicLink -Path Link created a
link to the current folder.

For a link that they can't create, they write a non-terminating
CreateHardLinkError or CreateSymbolicLinkError with the category
ResourceExists, ObjectNotFound, InvalidArgument, PermissionDenied, or
WriteError, and continue with the next object from the pipeline; they
stopped with a terminating error.

Fixed on the way: every object piped to the cmdlets failed with
GetDefaultValueFailed, because PowerShell reads a parameter that takes
pipeline input before it binds the input, and the getter of -Path threw
on the empty list. The pages show piping rows of a CSV file.

BREAKING CHANGE: a script that omits -Path or -Target gets a prompt, or
an error in a non-interactive session, and a script that relies on a
terminating error of the link cmdlets needs -ErrorAction Stop.

Decision 22, items 7 and 8: assumptions in autopilot, flagged for the
maintainer's review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
pull/116/head
Raimund Andree 3 days ago
parent
commit
3d309790ad
  1. 12
      CHANGELOG.md
  2. 24
      Docs/Cmdlets/New-NTFSHardLink.md
  3. 24
      Docs/Cmdlets/New-NTFSSymbolicLink.md
  4. 64
      NTFSSecurity/LinkCmdlets/NewHardLink.cs
  5. 47
      NTFSSecurity/LinkCmdlets/NewSymbolicLink.cs
  6. 45
      NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
  7. 79
      Tests/Links.Tests.ps1

12
CHANGELOG.md

@ -77,6 +77,15 @@ The format is based on
administrators of the named computer and the members of its group Access
Control Assistance Operators; any other account gets the error "Access is
denied" and no result
- **Breaking:** require `-Path` and `-Target` in `New-NTFSHardLink` and
`New-NTFSSymbolicLink`. Without `-Path`, they failed with an index error;
without `-Target`, they used the current location, so
`New-NTFSSymbolicLink -Path Link` created a link to the current folder
- **Breaking:** write a non-terminating error in `New-NTFSHardLink` and
`New-NTFSSymbolicLink` for a link that they can't create, such as for an
existing `-Path` or a missing `-Target`, and continue with the next link;
they stopped with a terminating error. A script that relies on the stop
needs `-ErrorAction Stop`
- Name the computer in the warning of `Get-NTFSEffectiveAccess` when the
computer of `-ServerName` can't be reached
@ -345,5 +354,8 @@ The format is based on
files failed with an error that named one of its files. It now writes a
`MoveError` that names the folder and the destination, and leaves the
folder in place
- Fix `New-NTFSHardLink` and `New-NTFSSymbolicLink`, which failed with
`GetDefaultValueFailed` for every object piped to them, such as the rows
of a CSV file with the columns `Path` and `Target`
[Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD

24
Docs/Cmdlets/New-NTFSHardLink.md

@ -14,15 +14,17 @@ Creates a hard link to an existing file.
## SYNTAX
```
New-NTFSHardLink [[-Path] <String>] [[-Target] <String>] [-PassThru] [<CommonParameters>]
New-NTFSHardLink [-Path] <String> [-Target] <String> [-PassThru] [<CommonParameters>]
```
## DESCRIPTION
The `New-NTFSHardLink` cmdlet gives an existing file an additional name. `-Path` is the new hard link that the cmdlet creates, and `-Target` is the existing file that the new link refers to. Read the command as "create *Path*, which points to *Target*".
The `New-NTFSHardLink` cmdlet gives an existing file an additional name. `-Path` is the new hard link that the cmdlet creates, and `-Target` is the existing file that the new link refers to. Read the command as "create *Path*, which points to *Target*". Both parameters are required.
The cmdlet validates both ends before it creates the link. `-Path` must not exist yet, so the cmdlet never overwrites an existing file, and `-Target` must exist and must be a file. A folder as `-Target` is rejected, because NTFS supports hard links for files only. Relative paths are resolved against the current location.
To create several links, pipe objects with the properties `Path` and `Target` to the cmdlet, one link per object. For a link that it can't create, the cmdlet writes a non-terminating error and continues with the next object.
After the link is created, both names refer to the same data on the volume. Writing through one name changes what the other name returns, and the file is only released when its last name is deleted.
By default the cmdlet produces no output. With `-PassThru` it returns one object for every hard link that the file has after the operation, which includes the original name and the new link, not just the link that was created.
@ -61,6 +63,14 @@ PS C:\> Get-NTFSHardLink -Path C:\Data\Report-2026.txt | Select-Object -ExpandPr
This command lists all names of the file after the link was created, which is the same information that `-PassThru` returns.
### Example 5: Create several links from a list
```PowerShell
PS C:\> Import-Csv -Path C:\Data\Links.csv | New-NTFSHardLink
```
This command creates one hard link for each row of `Links.csv`, which has the columns `Path` and `Target`. For a row whose link it can't create, the cmdlet writes an error and continues with the next row.
## PARAMETERS
### -PassThru
@ -88,7 +98,7 @@ Type: String
Parameter Sets: (All)
Aliases: FullName
Required: False
Required: True
Position: 1
Default value: None
Accept pipeline input: True (ByPropertyName, ByValue)
@ -104,7 +114,7 @@ Type: String
Parameter Sets: (All)
Aliases:
Required: False
Required: True
Position: 2
Default value: None
Accept pipeline input: True (ByPropertyName, ByValue)
@ -118,7 +128,7 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
### System.String
You can pass the path of the new link and the path of the target as strings.
You can pass the path of the new link and the path of the target as strings, or pipe objects whose `Path` or `FullName` property names the new link and whose `Target` property names its target.
## OUTPUTS
@ -136,12 +146,14 @@ Windows supports hard links only for files on the same NTFS volume. A link that
The cmdlet creates hard links on a network share as well, but Windows can't list the names of a file there. With `-PassThru` on a share, the cmdlet creates the link and writes a non-terminating `GetHardLinkError` with the message "The request is not supported" instead of the objects. Before 5.0.0, it stopped with a terminating error after it had created the link.
The cmdlet does not overwrite anything. If `-Path` already exists, or if `-Target` is missing or is a folder, the cmdlet reports an error and leaves the file system unchanged.
The cmdlet does not overwrite anything. If `-Path` already exists, or if `-Target` is missing or is a folder, the cmdlet writes a non-terminating `CreateHardLinkError` with the category `ResourceExists`, `ObjectNotFound`, or `InvalidArgument`, leaves the file system unchanged, and continues with the next object from the pipeline. When Windows refuses the link, such as for a target on another volume, the cmdlet writes a `CreateHardLinkError` as well.
Because all names of a file share the same data, the number of hard links is a property of the file, not of an individual name. Use `Get-NTFSHardLink` to list them, and delete a link with `Remove-Item2` or `Remove-Item`, which removes only that name as long as other names remain.
Before 5.0.0, the error for a missing `-Target` said "The target path exist", the opposite of the cause.
Before 5.0.0-rc7, `-Path` and `-Target` were optional: without `-Path`, the cmdlet failed with an index error, and without `-Target`, it used the current location, which is a folder. It stopped with a terminating error for an existing `-Path`, a missing `-Target`, a folder as `-Target`, or a link that Windows refused, and every object piped to it failed with `GetDefaultValueFailed`.
## RELATED LINKS
[Get-NTFSHardLink](Get-NTFSHardLink.md)

24
Docs/Cmdlets/New-NTFSSymbolicLink.md

@ -14,17 +14,19 @@ Creates a symbolic link to an existing file or folder.
## SYNTAX
```
New-NTFSSymbolicLink [[-Path] <String>] [[-Target] <String>] [-PassThru] [<CommonParameters>]
New-NTFSSymbolicLink [-Path] <String> [-Target] <String> [-PassThru] [<CommonParameters>]
```
## DESCRIPTION
The `New-NTFSSymbolicLink` cmdlet creates a symbolic link that redirects to another file or folder. `-Path` is the new link that the cmdlet creates, and `-Target` is the existing item that the link points to. Read the command as "create *Path*, which points to *Target*".
The `New-NTFSSymbolicLink` cmdlet creates a symbolic link that redirects to another file or folder. `-Path` is the new link that the cmdlet creates, and `-Target` is the existing item that the link points to. Read the command as "create *Path*, which points to *Target*". Both parameters are required.
The cmdlet inspects the target first and creates a file symbolic link when the target is a file and a directory symbolic link when the target is a folder, so you do not select the link type yourself. `-Target` must exist when the link is created, and `-Path` must not exist yet, so the cmdlet never overwrites an existing item.
Relative paths are resolved against the current location before the link is created, which means that the link always stores an absolute target path.
To create several links, pipe objects with the properties `Path` and `Target` to the cmdlet, one link per object. For a link that it can't create, the cmdlet writes a non-terminating error and continues with the next object.
By default the cmdlet produces no output. With `-PassThru` it returns an object for the new link: a file object for a link to a file, and a folder object for a link to a folder.
## EXAMPLES
@ -61,6 +63,14 @@ PS C:\> Test-Path2 -Path C:\Data\Current\Report.txt -PathType Leaf
This command tests a path that leads through the symbolic link. It returns `$true` when the link resolves and the file exists in the target folder.
### Example 5: Create several links from a list
```PowerShell
PS C:\> Import-Csv -Path C:\Data\Links.csv | New-NTFSSymbolicLink
```
This command creates one symbolic link for each row of `Links.csv`, which has the columns `Path` and `Target`. For a row whose link it can't create, the cmdlet writes an error and continues with the next row.
## PARAMETERS
### -PassThru
@ -88,7 +98,7 @@ Type: String
Parameter Sets: (All)
Aliases: FullName
Required: False
Required: True
Position: 1
Default value: None
Accept pipeline input: True (ByPropertyName, ByValue)
@ -104,7 +114,7 @@ Type: String
Parameter Sets: (All)
Aliases:
Required: False
Required: True
Position: 2
Default value: None
Accept pipeline input: True (ByPropertyName, ByValue)
@ -118,7 +128,7 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
### System.String
You can pass the path of the new link and the path of the target as strings.
You can pass the path of the new link and the path of the target as strings, or pipe objects whose `Path` or `FullName` property names the new link and whose `Target` property names its target.
## OUTPUTS
@ -136,8 +146,12 @@ Creating a symbolic link on Windows requires the "Create symbolic links" user ri
Unlike a hard link, a symbolic link is a separate file system entry that stores a path, so it can point to an item on another volume and the link and its target can be managed independently. The cmdlet still requires the target to exist at the moment the link is created. If the target is removed later, the link remains and stops resolving.
If `-Path` already exists or `-Target` is missing, the cmdlet writes a non-terminating `CreateSymbolicLinkError` with the category `ResourceExists` or `ObjectNotFound`, leaves the file system unchanged, and continues with the next object from the pipeline. When Windows refuses the link, such as with error 1314 without the right to create symbolic links, the cmdlet writes a `CreateSymbolicLinkError` as well.
Deleting a symbolic link removes the link only and leaves the target untouched. Delete a directory symbolic link as a link rather than recursively, so that the content of the target folder is not affected.
Before 5.0.0-rc7, `-Path` and `-Target` were optional: without `-Path`, the cmdlet failed with an index error, and without `-Target`, it created a link to the current folder. It stopped with a terminating error for an existing `-Path` or a link that Windows refused, and every object piped to it failed with `GetDefaultValueFailed`.
## RELATED LINKS
[New-NTFSHardLink](New-NTFSHardLink.md)

64
NTFSSecurity/LinkCmdlets/NewHardLink.cs

@ -14,13 +14,17 @@ namespace NTFSSecurity
private bool passThru;
System.Reflection.MethodInfo modeMethodInfo = null;
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
// Required since 5.0.0-rc7. Before, an omitted -Path failed with an index error, and an omitted -Target meant the
// current location, which is a folder.
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string Path
{
get { return paths[0]; }
// PowerShell reads a parameter that takes pipeline input before it binds the input. Before 5.0.0-rc7, the
// empty list failed that read, so every piped object failed with GetDefaultValueFailed.
get { return paths.Count > 0 ? paths[0] : null; }
set
{
paths.Clear();
@ -28,7 +32,7 @@ namespace NTFSSecurity
}
}
[Parameter(Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[Parameter(Mandatory = true, Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[FileSystemPathTransformation]
public string Target
@ -53,26 +57,43 @@ namespace NTFSSecurity
protected override void ProcessRecord()
{
var path = paths[0];
path = GetRelativePath(path);
target = GetRelativePath(target);
var path = GetRelativePath(paths[0]);
var targetPath = GetRelativePath(target);
var root = System.IO.Path.GetPathRoot(path);
try
{
// Non-terminating errors, so that the links that follow in the pipeline are created as well. Before
// 5.0.0-rc7, an existing path, a missing target, or a folder as target stopped the pipeline.
FileSystemInfo temp = null;
if (TryGetFileSystemInfo2(path, out temp))
throw new ArgumentException(string.Format("The file '{0}' does already exist, cannot create the link", path));
{
var exists = new ArgumentException(string.Format("The file '{0}' does already exist, cannot create the link", path));
WriteError(new ErrorRecord(exists, "CreateHardLinkError", ErrorCategory.ResourceExists, path));
return;
}
if (!TryGetFileSystemInfo2(targetPath, out temp))
{
var missing = new System.IO.FileNotFoundException(string.Format("The target '{0}' does not exist, cannot create the link", targetPath), targetPath);
WriteError(new ErrorRecord(missing, "CreateHardLinkError", ErrorCategory.ObjectNotFound, path));
return;
}
if (!TryGetFileSystemInfo2(target, out temp))
throw new ArgumentException(string.Format("The target '{0}' does not exist, cannot create the link", target));
else
if (temp is DirectoryInfo)
throw new ArgumentException("The target is not a file, cannot create the link");
{
var folder = new ArgumentException("The target is not a file, cannot create the link");
WriteError(new ErrorRecord(folder, "CreateHardLinkError", ErrorCategory.InvalidArgument, path));
return;
}
File.CreateHardlink(path, target);
try
{
File.CreateHardlink(path, targetPath);
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "CreateHardLinkError", ex is UnauthorizedAccessException ? ErrorCategory.PermissionDenied : ErrorCategory.WriteError, path));
return;
}
if (passThru)
{
@ -91,16 +112,11 @@ namespace NTFSSecurity
foreach (var link in links)
{
var target = new PSObject(GetFileSystemInfo2(System.IO.Path.Combine(root, link.Substring(1))));
target.Properties.Add(new PSCodeProperty("Mode", modeMethodInfo));
WriteObject(target);
}
var name = new PSObject(GetFileSystemInfo2(System.IO.Path.Combine(root, link.Substring(1))));
name.Properties.Add(new PSCodeProperty("Mode", modeMethodInfo));
WriteObject(name);
}
}
catch (System.IO.FileNotFoundException ex)
{
WriteError(new ErrorRecord(ex, "CreateHardLinkError", ErrorCategory.WriteError, path));
}
}
protected override void EndProcessing()

47
NTFSSecurity/LinkCmdlets/NewSymbolicLink.cs

@ -12,13 +12,17 @@ namespace NTFSSecurity
private bool passThru;
System.Reflection.MethodInfo modeMethodInfo = null;
[Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
// Required since 5.0.0-rc7. Before, an omitted -Path failed with an index error, and an omitted -Target meant the
// current location, so the cmdlet created a link to the current folder.
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("FullName")]
[FileSystemPathTransformation]
public string Path
{
get { return paths[0]; }
// PowerShell reads a parameter that takes pipeline input before it binds the input. Before 5.0.0-rc7, the
// empty list failed that read, so every piped object failed with GetDefaultValueFailed.
get { return paths.Count > 0 ? paths[0] : null; }
set
{
paths.Clear();
@ -26,7 +30,7 @@ namespace NTFSSecurity
}
}
[Parameter(Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[Parameter(Mandatory = true, Position = 2, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[FileSystemPathTransformation]
public string Target
@ -51,24 +55,40 @@ namespace NTFSSecurity
protected override void ProcessRecord()
{
var path = paths[0];
var path = GetRelativePath(paths[0]);
var targetPath = GetRelativePath(target);
path = GetRelativePath(path);
target = GetRelativePath(target);
// Non-terminating errors, so that the links that follow in the pipeline are created as well. Before
// 5.0.0-rc7, an existing path and a failure to create the link stopped the pipeline.
FileSystemInfo targetItem = null;
var root = System.IO.Path.GetPathRoot(path);
try
{
targetItem = GetFileSystemInfo2(target);
targetItem = GetFileSystemInfo2(targetPath);
}
catch (System.IO.FileNotFoundException ex)
{
WriteError(new ErrorRecord(ex, "CreateSymbolicLinkError", ErrorCategory.ObjectNotFound, path));
return;
}
FileSystemInfo temp;
if (TryGetFileSystemInfo2(path, out temp))
{
throw new ArgumentException("The path does already exist, cannot create link");
var exists = new ArgumentException("The path does already exist, cannot create link");
WriteError(new ErrorRecord(exists, "CreateSymbolicLinkError", ErrorCategory.ResourceExists, path));
return;
}
File.CreateSymbolicLink(path, target, targetItem is FileInfo ? SymbolicLinkTarget.File : SymbolicLinkTarget.Directory);
try
{
File.CreateSymbolicLink(path, targetPath, targetItem is FileInfo ? SymbolicLinkTarget.File : SymbolicLinkTarget.Directory);
}
// Without the right to create symbolic links: (1314) A required privilege is not held by the client.
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, "CreateSymbolicLinkError", ex is UnauthorizedAccessException ? ErrorCategory.PermissionDenied : ErrorCategory.WriteError, path));
return;
}
if (passThru)
{
@ -78,11 +98,6 @@ namespace NTFSSecurity
WriteObject(new DirectoryInfo(path));
}
}
catch (System.IO.FileNotFoundException ex)
{
WriteError(new ErrorRecord(ex, "CreateSymbolicLinkError", ErrorCategory.ObjectNotFound, path));
}
}
protected override void EndProcessing()
{

45
NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml

@ -7053,15 +7053,16 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</maml:description>
</command:details>
<maml:description>
<maml:para>The `New-NTFSHardLink` cmdlet gives an existing file an additional name. `-Path` is the new hard link that the cmdlet creates, and `-Target` is the existing file that the new link refers to. Read the command as "create Path , which points to Target ".</maml:para>
<maml:para>The `New-NTFSHardLink` cmdlet gives an existing file an additional name. `-Path` is the new hard link that the cmdlet creates, and `-Target` is the existing file that the new link refers to. Read the command as "create Path , which points to Target ". Both parameters are required.</maml:para>
<maml:para>The cmdlet validates both ends before it creates the link. `-Path` must not exist yet, so the cmdlet never overwrites an existing file, and `-Target` must exist and must be a file. A folder as `-Target` is rejected, because NTFS supports hard links for files only. Relative paths are resolved against the current location.</maml:para>
<maml:para>To create several links, pipe objects with the properties `Path` and `Target` to the cmdlet, one link per object. For a link that it can't create, the cmdlet writes a non-terminating error and continues with the next object.</maml:para>
<maml:para>After the link is created, both names refer to the same data on the volume. Writing through one name changes what the other name returns, and the file is only released when its last name is deleted.</maml:para>
<maml:para>By default the cmdlet produces no output. With `-PassThru` it returns one object for every hard link that the file has after the operation, which includes the original name and the new link, not just the link that was created.</maml:para>
</maml:description>
<command:syntax>
<command:syntaxItem>
<maml:name>New-NTFSHardLink</maml:name>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName">
<command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName">
<maml:name>Path</maml:name>
<maml:description>
<maml:para>Specifies the path of the new hard link that the cmdlet creates. The path must not exist yet, and it must be on the same NTFS volume as `-Target`. Relative paths are resolved against the current location.</maml:para>
@ -7073,7 +7074,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</dev:type>
<dev:defaultValue>None</dev:defaultValue>
</command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none">
<command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none">
<maml:name>Target</maml:name>
<maml:description>
<maml:para>Specifies the path of the existing file that the new link refers to. The target must exist and must be a file; folders are rejected, because NTFS supports hard links for files only.</maml:para>
@ -7111,7 +7112,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</dev:type>
<dev:defaultValue>False</dev:defaultValue>
</command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName">
<command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName">
<maml:name>Path</maml:name>
<maml:description>
<maml:para>Specifies the path of the new hard link that the cmdlet creates. The path must not exist yet, and it must be on the same NTFS volume as `-Target`. Relative paths are resolved against the current location.</maml:para>
@ -7123,7 +7124,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</dev:type>
<dev:defaultValue>None</dev:defaultValue>
</command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none">
<command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none">
<maml:name>Target</maml:name>
<maml:description>
<maml:para>Specifies the path of the existing file that the new link refers to. The target must exist and must be a file; folders are rejected, because NTFS supports hard links for files only.</maml:para>
@ -7142,7 +7143,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:name>System.String</maml:name>
</dev:type>
<maml:description>
<maml:para>You can pass the path of the new link and the path of the target as strings.</maml:para>
<maml:para>You can pass the path of the new link and the path of the target as strings, or pipe objects whose `Path` or `FullName` property names the new link and whose `Target` property names its target.</maml:para>
</maml:description>
</command:inputType>
</command:inputTypes>
@ -7168,9 +7169,10 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:alert>
<maml:para>Windows supports hard links only for files on the same NTFS volume. A link that points to a file on another volume, or a target on a file system that does not implement hard links, cannot be created.</maml:para>
<maml:para>The cmdlet creates hard links on a network share as well, but Windows can't list the names of a file there. With `-PassThru` on a share, the cmdlet creates the link and writes a non-terminating `GetHardLinkError` with the message "The request is not supported" instead of the objects. Before 5.0.0, it stopped with a terminating error after it had created the link.</maml:para>
<maml:para>The cmdlet does not overwrite anything. If `-Path` already exists, or if `-Target` is missing or is a folder, the cmdlet reports an error and leaves the file system unchanged.</maml:para>
<maml:para>The cmdlet does not overwrite anything. If `-Path` already exists, or if `-Target` is missing or is a folder, the cmdlet writes a non-terminating `CreateHardLinkError` with the category `ResourceExists`, `ObjectNotFound`, or `InvalidArgument`, leaves the file system unchanged, and continues with the next object from the pipeline. When Windows refuses the link, such as for a target on another volume, the cmdlet writes a `CreateHardLinkError` as well.</maml:para>
<maml:para>Because all names of a file share the same data, the number of hard links is a property of the file, not of an individual name. Use `Get-NTFSHardLink` to list them, and delete a link with `Remove-Item2` or `Remove-Item`, which removes only that name as long as other names remain.</maml:para>
<maml:para>Before 5.0.0, the error for a missing `-Target` said "The target path exist", the opposite of the cause.</maml:para>
<maml:para>Before 5.0.0-rc7, `-Path` and `-Target` were optional: without `-Path`, the cmdlet failed with an index error, and without `-Target`, it used the current location, which is a folder. It stopped with a terminating error for an existing `-Path`, a missing `-Target`, a folder as `-Target`, or a link that Windows refused, and every object piped to it failed with `GetDefaultValueFailed`.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
@ -7202,6 +7204,13 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:para>This command lists all names of the file after the link was created, which is the same information that `-PassThru` returns.</maml:para>
</dev:remarks>
</command:example>
<command:example>
<maml:title>--------- Example 5: Create several links from a list ---------</maml:title>
<dev:code>PS C:\&gt; Import-Csv -Path C:\Data\Links.csv | New-NTFSHardLink</dev:code>
<dev:remarks>
<maml:para>This command creates one hard link for each row of `Links.csv`, which has the columns `Path` and `Target`. For a row whose link it can't create, the cmdlet writes an error and continues with the next row.</maml:para>
</dev:remarks>
</command:example>
</command:examples>
<command:relatedLinks>
<maml:navigationLink>
@ -7236,15 +7245,16 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</maml:description>
</command:details>
<maml:description>
<maml:para>The `New-NTFSSymbolicLink` cmdlet creates a symbolic link that redirects to another file or folder. `-Path` is the new link that the cmdlet creates, and `-Target` is the existing item that the link points to. Read the command as "create Path , which points to Target ".</maml:para>
<maml:para>The `New-NTFSSymbolicLink` cmdlet creates a symbolic link that redirects to another file or folder. `-Path` is the new link that the cmdlet creates, and `-Target` is the existing item that the link points to. Read the command as "create Path , which points to Target ". Both parameters are required.</maml:para>
<maml:para>The cmdlet inspects the target first and creates a file symbolic link when the target is a file and a directory symbolic link when the target is a folder, so you do not select the link type yourself. `-Target` must exist when the link is created, and `-Path` must not exist yet, so the cmdlet never overwrites an existing item.</maml:para>
<maml:para>Relative paths are resolved against the current location before the link is created, which means that the link always stores an absolute target path.</maml:para>
<maml:para>To create several links, pipe objects with the properties `Path` and `Target` to the cmdlet, one link per object. For a link that it can't create, the cmdlet writes a non-terminating error and continues with the next object.</maml:para>
<maml:para>By default the cmdlet produces no output. With `-PassThru` it returns an object for the new link: a file object for a link to a file, and a folder object for a link to a folder.</maml:para>
</maml:description>
<command:syntax>
<command:syntaxItem>
<maml:name>New-NTFSSymbolicLink</maml:name>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName">
<command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName">
<maml:name>Path</maml:name>
<maml:description>
<maml:para>Specifies the path of the new symbolic link that the cmdlet creates. The path must not exist yet. Relative paths are resolved against the current location.</maml:para>
@ -7256,7 +7266,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</dev:type>
<dev:defaultValue>None</dev:defaultValue>
</command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none">
<command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none">
<maml:name>Target</maml:name>
<maml:description>
<maml:para>Specifies the path of the existing file or folder that the new link points to. The target must exist when the link is created and determines whether the cmdlet creates a file symbolic link or a directory symbolic link. Relative paths are resolved against the current location, so the link stores an absolute target path.</maml:para>
@ -7294,7 +7304,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</dev:type>
<dev:defaultValue>False</dev:defaultValue>
</command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName">
<command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="1" aliases="FullName">
<maml:name>Path</maml:name>
<maml:description>
<maml:para>Specifies the path of the new symbolic link that the cmdlet creates. The path must not exist yet. Relative paths are resolved against the current location.</maml:para>
@ -7306,7 +7316,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
</dev:type>
<dev:defaultValue>None</dev:defaultValue>
</command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none">
<command:parameter required="true" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName, ByValue)" position="2" aliases="none">
<maml:name>Target</maml:name>
<maml:description>
<maml:para>Specifies the path of the existing file or folder that the new link points to. The target must exist when the link is created and determines whether the cmdlet creates a file symbolic link or a directory symbolic link. Relative paths are resolved against the current location, so the link stores an absolute target path.</maml:para>
@ -7325,7 +7335,7 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:name>System.String</maml:name>
</dev:type>
<maml:description>
<maml:para>You can pass the path of the new link and the path of the target as strings.</maml:para>
<maml:para>You can pass the path of the new link and the path of the target as strings, or pipe objects whose `Path` or `FullName` property names the new link and whose `Target` property names its target.</maml:para>
</maml:description>
</command:inputType>
</command:inputTypes>
@ -7351,7 +7361,9 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:alert>
<maml:para>Creating a symbolic link on Windows requires the "Create symbolic links" user right, `SeCreateSymbolicLinkPrivilege`, which is granted to the Administrators group by default. Without that right, Windows rejects the operation with error 1314, "A required privilege is not held by the client", so run the cmdlet from an elevated session or grant the right to the account. Windows Developer Mode doesn't change this: it lets accounts without that right create symbolic links only in programs that request it, such as `mklink`, and the cmdlet doesn't.</maml:para>
<maml:para>Unlike a hard link, a symbolic link is a separate file system entry that stores a path, so it can point to an item on another volume and the link and its target can be managed independently. The cmdlet still requires the target to exist at the moment the link is created. If the target is removed later, the link remains and stops resolving.</maml:para>
<maml:para>If `-Path` already exists or `-Target` is missing, the cmdlet writes a non-terminating `CreateSymbolicLinkError` with the category `ResourceExists` or `ObjectNotFound`, leaves the file system unchanged, and continues with the next object from the pipeline. When Windows refuses the link, such as with error 1314 without the right to create symbolic links, the cmdlet writes a `CreateSymbolicLinkError` as well.</maml:para>
<maml:para>Deleting a symbolic link removes the link only and leaves the target untouched. Delete a directory symbolic link as a link rather than recursively, so that the content of the target folder is not affected.</maml:para>
<maml:para>Before 5.0.0-rc7, `-Path` and `-Target` were optional: without `-Path`, the cmdlet failed with an index error, and without `-Target`, it created a link to the current folder. It stopped with a terminating error for an existing `-Path` or a link that Windows refused, and every object piped to it failed with `GetDefaultValueFailed`.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
@ -7383,6 +7395,13 @@ PS C:\Data&gt; Get-NTFSSecurityDescriptor</dev:code>
<maml:para>This command tests a path that leads through the symbolic link. It returns `$true` when the link resolves and the file exists in the target folder.</maml:para>
</dev:remarks>
</command:example>
<command:example>
<maml:title>--------- Example 5: Create several links from a list ---------</maml:title>
<dev:code>PS C:\&gt; Import-Csv -Path C:\Data\Links.csv | New-NTFSSymbolicLink</dev:code>
<dev:remarks>
<maml:para>This command creates one symbolic link for each row of `Links.csv`, which has the columns `Path` and `Target`. For a row whose link it can't create, the cmdlet writes an error and continues with the next row.</maml:para>
</dev:remarks>
</command:example>
</command:examples>
<command:relatedLinks>
<maml:navigationLink>

79
Tests/Links.Tests.ps1

@ -122,6 +122,36 @@ Describe 'New-NTFSHardLink' {
$link | Should -Not -Exist
}
# Before 5.0.0-rc7, an existing -Path, a missing -Target, or a folder as -Target stopped the pipeline with a
# terminating error, so the links that followed weren't created.
It 'Should write a non-terminating error for each link that it cannot create and continue with the next one' {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueTarget'
$existing = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueExisting'
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ContinueFolder' -Directory
$missing = Join-Path -Path $sandbox -ChildPath 'ContinueMissing.txt'
$missingLink = Join-Path -Path $sandbox -ChildPath 'ContinueMissingLink.txt'
$folderLink = Join-Path -Path $sandbox -ChildPath 'ContinueFolderLink.txt'
$link = Join-Path -Path $sandbox -ChildPath 'ContinueLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing, $missingLink, $folderLink, $link
$requests = @(
[pscustomobject]@{ Path = $existing; Target = $target }
[pscustomobject]@{ Path = $missingLink; Target = $missing }
[pscustomobject]@{ Path = $folderLink; Target = $folder }
[pscustomobject]@{ Path = $link; Target = $target }
)
$requests | New-NTFSHardLink -ErrorVariable linkErrors -ErrorAction SilentlyContinue
$linkErrors | Should -HaveCount 3
$linkErrors | ForEach-Object -Process { $_.FullyQualifiedErrorId | Should -BeLike 'CreateHardLinkError,*' }
($linkErrors | ForEach-Object -Process { $_.CategoryInfo.Category }) -join ',' | Should -Be 'ResourceExists,ObjectNotFound,InvalidArgument'
($linkErrors | ForEach-Object -Process { $_.TargetObject }) -join '|' | Should -Be (($existing, $missingLink, $folderLink) -join '|')
$link | Should -Exist
$missingLink | Should -Not -Exist
$folderLink | Should -Not -Exist
Get-Content -LiteralPath $existing | Should -Be 'ContinueExisting'
}
# Windows can't list the names of a file on a network share. Before 5.0.0-rc6, the cmdlet stopped with a
# terminating error after it had created the link.
It 'Should create the link on a network share and write an error for -PassThru, which cannot list the names there' -Skip:(-not $canUseAdminShare) {
@ -309,17 +339,56 @@ Describe 'New-NTFSSymbolicLink' {
Test-Path2 -Path $link | Should -BeFalse
}
# Before 5.0.0-rc7, an existing -Path stopped the pipeline with a terminating error. The cmdlet checks the paths
# before it creates a link, so this runs without the right to create symbolic links as well.
It 'Should write a non-terminating error for an existing -Path and continue with the next link' {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicContinueTarget'
$existing = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicContinueExisting'
$missing = Join-Path -Path $sandbox -ChildPath 'SymbolicContinueMissing.txt'
$link = Join-Path -Path $sandbox -ChildPath 'SymbolicContinueLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing, $link
$requests = @(
[pscustomobject]@{ Path = $existing; Target = $target }
[pscustomobject]@{ Path = $link; Target = $missing }
)
$requests | New-NTFSSymbolicLink -ErrorVariable linkErrors -ErrorAction SilentlyContinue
$linkErrors | Should -HaveCount 2
$linkErrors | ForEach-Object -Process { $_.FullyQualifiedErrorId | Should -BeLike 'CreateSymbolicLinkError,*' }
($linkErrors | ForEach-Object -Process { $_.CategoryInfo.Category }) -join ',' | Should -Be 'ResourceExists,ObjectNotFound'
(Get-Item -LiteralPath $existing -Force).LinkType | Should -BeNullOrEmpty
Test-Path2 -Path $link | Should -BeFalse
}
# Windows rejects the link with error 1314 without the "Create symbolic links" user right. Its message is
# localized, so the test compares the HRESULT of that error.
It 'Should fail and create no link without the right to create symbolic links' -Skip:$canCreateSymbolicLinks {
# localized, so the test compares the HRESULT of that error. Before 5.0.0-rc7, the error was terminating.
It 'Should write a non-terminating error and create no link without the right to create symbolic links' -Skip:$canCreateSymbolicLinks {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicNoRight'
$link = Join-Path -Path $sandbox -ChildPath 'SymbolicNoRight.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
$thrown = { New-NTFSSymbolicLink -Path $link -Target $target -ErrorAction Stop } | Should -Throw -PassThru
New-NTFSSymbolicLink -Path $link -Target $target -ErrorVariable linkErrors -ErrorAction SilentlyContinue
$thrown.FullyQualifiedErrorId | Should -BeLike '*,NTFSSecurity.NewSymbolicLink'
'0x{0:X8}' -f $thrown.Exception.HResult | Should -Be '0x80070522'
$linkErrors | Should -HaveCount 1
$linkErrors[0].FullyQualifiedErrorId | Should -BeLike 'CreateSymbolicLinkError,*'
'0x{0:X8}' -f $linkErrors[0].Exception.HResult | Should -Be '0x80070522'
Test-Path2 -Path $link | Should -BeFalse
}
}
# Before 5.0.0-rc7, both parameters were optional. Without -Path, the cmdlets failed with an index error; without -Target,
# they used the current location, so New-NTFSSymbolicLink -Path Link created a link to the current folder.
Describe 'Parameters of the cmdlets that create links' {
It '<Command> should require -<Parameter>' -ForEach @(
@{ Command = 'New-NTFSHardLink'; Parameter = 'Path' }
@{ Command = 'New-NTFSHardLink'; Parameter = 'Target' }
@{ Command = 'New-NTFSSymbolicLink'; Parameter = 'Path' }
@{ Command = 'New-NTFSSymbolicLink'; Parameter = 'Target' }
) {
$sets = @((Get-Command -Name $Command).Parameters[$Parameter].ParameterSets.Values)
$sets | Should -Not -BeNullOrEmpty
$sets | ForEach-Object -Process { $_.IsMandatory | Should -BeTrue }
}
}

Loading…
Cancel
Save