Powershell: Feature Request: Add -IfNotExists switch to New-Item

Created on 25 Jan 2020  Â·  15Comments  Â·  Source: PowerShell/PowerShell

Summary of the new feature/enhancement

New-Item cmdlet can be used to create an empty file. However, it gives an error if the file already exists. Sometimes, you just want to ensure the file exists – create it if it doesn't, do nothing if it does. Of course, one could do this with an if conditional but having just a switch to do it would make things a lot easier.

(This is different from -Force – -Force replaces the existing file with an empty file, -IfNotExists would leave the existing file unmodified.)

Sometimes it is suggested that New-Item is equivalent to Unix touch. However, touch leaves existing files intact, New-Item obliterates them. If New-Item had an -IfNotExists option, it would be closer to touch.

Proposed technical implementation details (optional)

Area-Cmdlets-Management Issue-Enhancement

Most helpful comment

tl;dr:

  • Good idea, but the switch should be named -NoClobber for consistency with existing switches with essentially the same purpose - this switch should _not_ modify the timestamp of an existing item.

  • I suggest leaving timestamp-related functionality out of New-Item.


There are two independent aspects:

  • ensuring a file's _existence_

  • modifying a file's _timestamps_

Even though the Unix touch utility combines the two aspects (in its argument-less form, it ensures the existence of the file _and_ updates the last-write timestamp if the file already exists), I don't think that's appropriate for New-Item, given its name and purpose.

The focus of touch is _timestamp functionality_, whereas New-Item's is _existence_.

touch has numerous useful options, and fundamentally the ability to target _multiple_ items, via wildcards; the best we could do with a dynamic -Touch parameter is to provide a limited, single-item subset of touch's functionality - and I don't think that's worth it.

Food for thought for a fully featured potential PowerShell implementation of the Unix touch utility is in the Touch-Item implementation from this Stack Overflow answer; note that I deliberately chose to go with non-standard verb Touch, because I couldn't come up with a succinct yet meaningful name using an approved verb.


md $path -erroraction SilentlyContinue

As @skissane notes, -Force _truncates_ the item if it is a _file_, but if it is a _directory_, it leaves an existing one alone (including its time stamps), hence:

# Create the directory, unless it already exists.
# Note: on Unix-like platforms 'md' and 'mkdir' refer to the Unix mkdir utility, 
# not New-Item.
New-Item -Type Directory -Force $path

This asymmetry of the -Force behavior is unfortunate, but were's stuck with it.

Therefore, for directories the new -NoClobber switch would have to have the same effect as -Force, and would arguably be the better switch to use going forward, to better signal the intent. - see below.

All 15 comments

I'd name the parameter -TouchFile. "Touch" says about expected behavior, "File" says that the parameter is a dynamic parameter for FileSystem provider.

Update: -Touch \

/cc @mklement0

It's a useful thing to be able to do.
Currently you can do if (-not (test-path $path)) {new-item $Path}so this would make the syntax neater, rather than making impossible things possible. .
It needs to have the right behaviour for different item types. -TouchFile for files only is OK , but something more generic for directories etc. might be better.
I commonly find I write
md $path -erroraction SilentlyContinue
really I _should_ use try {} catch{}and ignore "exists" errors , but stop for "failed to add" ones. Some sort of "Ensure-there-is " which errors if it can't make one would make for nicer code

Making a simple powerShell function which is the equivalent of unix's touch on Windows would be the natural next thing to do.

It needs to have the right behaviour for different item types. -TouchFile for files only is OK , but something more generic for directories etc. might be better.

Makes sense.
Also I found that touch utility can accept optional timestamp.
If it will be a dynamic parameter for FileSystem provider the parameter could be -Touch <optional datetime>.

tl;dr:

  • Good idea, but the switch should be named -NoClobber for consistency with existing switches with essentially the same purpose - this switch should _not_ modify the timestamp of an existing item.

  • I suggest leaving timestamp-related functionality out of New-Item.


There are two independent aspects:

  • ensuring a file's _existence_

  • modifying a file's _timestamps_

Even though the Unix touch utility combines the two aspects (in its argument-less form, it ensures the existence of the file _and_ updates the last-write timestamp if the file already exists), I don't think that's appropriate for New-Item, given its name and purpose.

The focus of touch is _timestamp functionality_, whereas New-Item's is _existence_.

touch has numerous useful options, and fundamentally the ability to target _multiple_ items, via wildcards; the best we could do with a dynamic -Touch parameter is to provide a limited, single-item subset of touch's functionality - and I don't think that's worth it.

Food for thought for a fully featured potential PowerShell implementation of the Unix touch utility is in the Touch-Item implementation from this Stack Overflow answer; note that I deliberately chose to go with non-standard verb Touch, because I couldn't come up with a succinct yet meaningful name using an approved verb.


md $path -erroraction SilentlyContinue

As @skissane notes, -Force _truncates_ the item if it is a _file_, but if it is a _directory_, it leaves an existing one alone (including its time stamps), hence:

# Create the directory, unless it already exists.
# Note: on Unix-like platforms 'md' and 'mkdir' refer to the Unix mkdir utility, 
# not New-Item.
New-Item -Type Directory -Force $path

This asymmetry of the -Force behavior is unfortunate, but were's stuck with it.

Therefore, for directories the new -NoClobber switch would have to have the same effect as -Force, and would arguably be the better switch to use going forward, to better signal the intent. - see below.

What about adding an Update-Item that works like touch?

Update sounds like a good choice for the verb, but Item is too vague, I think: the cmdlet wouldn't be updating the _item itself_, but _a piece of associated metadata_.

Perhaps Update-ItemTimestamp? Or just Update-Timestamp?

A compliant alias name for it would be udts (ud being the official alias name for the Update verb).

Re the term "Item": It suggests support for all providers, which I don't think we need or perhaps even can aim for; unfortunately, there is no better umbrella term for "file or folder" I can think of.

Update-Path

Perhaps Update-ItemTimestamp? Or just Update-Timestamp?

That makes most sense.

Back to the original question, I'm not sure how often we need to create an empty file, one can do
$null | Add-Content $path which does nothing to the file if it exists (doesn't change the date stamp) but creates a zero byte file if there is none.

  • Good idea, but the switch should be named -NoClobber for consistency with existing switches with essentially the same purpose - this switch should _not_ modify the timestamp of an existing item

New-item already seems to have -NoClobber by default -Force to overcome that deletes the file and creates a new one. Doing append rather than create seems to be the answer for files but for directories or other containers that doesn't make sense.

What's best depends on whether the aim is to fix general cases and have a switch which does "Ignore 'already exists' errors" or give something very similar to touch for files.

Update sounds like a good choice for the verb, but Item is too vague, I think: the cmdlet wouldn't be updating the _item itself_, but _a piece of associated metadata_.
(...)
Re the term "Item": It suggests support for all providers, which I don't think we need or perhaps even can aim for; unfortunately, there is no better umbrella term for "file or folder" I can think of.

Well, I think it would be up to the provider to determine what updating an item means. For example, lets say you have a cache provider (I think UD has one?), update item could refresh the cache expiration. Registry also has a lastwritetime iirc (I know one is recorded somewhere, can't remember if it's surfaced in the dotnet API).

Think of it like Invoke-Item. Plenty of providers do not implement an Invoke, but the cmdlet still makes sense.

Well, I think it would be up to the provider to determine what updating an item means.

Good point; what I was thinking of is updating an item's _content_, for which we do have the noun _Content_, such as in Set-Content.

If we can come up with a generic Update-Item implementation that meaningfully works for multiple providers, I won't complain, but I wonder if that would become too generic / hard to discover, because the term _timestamp_ would then be in the _parameter_ names.

Also, such a cross-provider cmdlet would have to have to implement parts of the touch-specific functionality via dynamic, filesystem-provider-specific parameters (last-access vs. last-write timestamp, treatment of symbolic links).

But it sounds like at this point we should create a new issue focused on implementing touch functionality in a separate cmdlet, and keep this issue focused on the -NoClobber aspect.

$null | Add-Content $path which does nothing to the file if it exists (doesn't change the date stamp)

That's a neat _workaround_, but far from obvious; I do think there is value in implementing _desired-state logic_ in this case (and in general): "Do what is necessary to make sure that this file exists - whether it's already there or whether you have to create it".

However, one case I hadn't considered before: if -Value is _also_ specified, -NoClobber should cause a statement-terminating error, as the two parameters conflict conceptually, given that New-Item - by design - when given a -Value - either truncates and fills or creates and fills, leaving only the specified -Value in the file (it shouldn't _append_; for that, we have Add-Content, which also creates the file on demand).

As an aside: specifying a -Value when creating a _directory_ - which arguably shouldn't be prevented _syntactically_ - is currently quietly ignored.

deletes the file and creates a new one.

Actually, it _truncates the existing file_ instead, which is not the same, because truncating preserves the file creation date, permissions, ownership, ...

You could argue that -Force should indeed delete and re-create, but truncating is the current behavior.

As for -NoClobber vs. -Force:

I hadn't consider that -Force has _another_ implication:

It creates _parent directories on demand_, so that you can use New-Item -Force noSuchSubDirYet/newFile and New-Item -Force noSuchSubDirYet/another/newDir.

I we say that -NoClobber means _only_: "leave an existing item alone, (do not also create parent directories on demand)", then adding -Force would be needed to ask for the latter, so that we get the following combinations:

switch(es) → / item type ↓ | -NoClobber | -Force | -NoClobber -Force
--------- | ------------ | ------- | ---------
File | if file exists, leave it alone, except fail if -Value is also specified; if parent path doesn't exist, fail | (current behavior) create parent path on demand; truncate file, if it exists | if file exists, leave it alone (except if -Value is also specified); create parent path on demand
Directory | if directory exists, leave it alone; if parent path doesn't exist, fail | (current behavior) if directory exists, leave it alone; if parent path doesn't exist, create it | same as -Force alone

Note that a slight inconsistency with respect to parameter name -NoClobber is that in the existing uses it usually results in an _error_ when the target file already exists, but, given the commonality of leaving an existing target untouched, I think it's still the right name.

P.S., @iSazonov:

Update-Path

In PowerShell, a _path_ is an _address_ of sorts that locates a provider _item_, as reflected in parameters -Path and -LiteralPath, so I don't think that's the way to go; it also has echoes of $env:PATH. "FileSystemItem" instead of "Item" would capture "file or directory", but that's too wordy.

I don't think a new cmdlet is needed to update the modified date time. Set-ItemProperty cmdlet can update LastWriteTime, LastAccessTime, CreationTime properties of a file.

C:\> Get-ItemProperty C:\temp\abc.psd1 -Name lastwritetime

lastwritetime : 1/6/2020 11:22:09 PM
PSPath        : Microsoft.PowerShell.Core\FileSystem::C:\temp\abc.psd1
PSParentPath  : Microsoft.PowerShell.Core\FileSystem::C:\temp
PSChildName   : abc.psd1
PSDrive       : C
PSProvider    : Microsoft.PowerShell.Core\FileSystem


C:\> Set-ItemProperty C:\temp\abc.psd1 -Name lastwritetime -Value (Get-Date)
C:\> Get-ItemProperty C:\temp\abc.psd1 -Name lastwritetime

lastwritetime : 1/26/2020 2:32:33 PM
PSPath        : Microsoft.PowerShell.Core\FileSystem::C:\temp\abc.psd1
PSParentPath  : Microsoft.PowerShell.Core\FileSystem::C:\temp
PSChildName   : abc.psd1
PSDrive       : C
PSProvider    : Microsoft.PowerShell.Core\FileSystem


C:\> Set-ItemProperty C:\temp\abc.psd1 -Name lastwritetime -Value (Get-Date).AddDays(-10)
C:\> Get-ItemProperty C:\temp\abc.psd1 -Name lastwritetime

lastwritetime : 1/16/2020 2:33:05 PM
PSPath        : Microsoft.PowerShell.Core\FileSystem::C:\temp\abc.psd1
PSParentPath  : Microsoft.PowerShell.Core\FileSystem::C:\temp
PSChildName   : abc.psd1
PSDrive       : C
PSProvider    : Microsoft.PowerShell.Core\FileSystem

Excellent point, @ThomasNieto - I completely forgot about Set-ItemProperty, because I rarely use it - but it effectively is the embodiment of what we discussed as an all-providers Update-Item.

This gives us most of what touch can do, except:

  • file creation on demand

  • being able to distinguish between modifying the properties of a symlink's _target_ and the _link itself_.

Arguably:

  • the former can be covered simply by having to call New-Item [-NoClobber] first.

  • the latter should be added as a dynamic filesystem-provider parameter to Set-ItemProperty, such as -ModifyLink (default would be to modify the link's _target_).

Given Set-ItemProperty's support for positional arguments and using its built-in alias sp, concise commands are possible in interactive use; e.g., the following sets the last-write date of all *.txt files in the current directory back by 1 day:

sp *.txt LastWriteTime (Get-Date).AddDays(-1)

In short: I'm no longer convinced we need a separate touch-like cmdlet.

Let me offer a pragmatic solution, which is easy to implement _as a custom solution_ now, also in Windows PowerShell, using either an available touch utility or an emulation of it (with the same basis syntax).

On Unix, the touch utility is automatically available, so no action is required - just call it directly.

On Windows, there are two options, which you could place in your $PROFILE:

  • On machines that have WSL installed, you can wrap a Unix distro's touch utility as follows:

    • function touch { wsl touch $args }
  • On other machines, you can use the function below, which emulates the _core_ functionality of the touch utility (no options supported).

# Define the `touch` function only if a `touch` utility isn't present (it always is on Unix).
if (-not (Get-Command -Type Application touch -ea Ignore)) {
  function touch {
    <#
    .SYNOPSIS
    Emulates the Unix touch utility.

    .DESCRIPTION
    On Windows, emulates the core functionality (only) of 
    the Unix touch utility; examples:

    touch new1.txt new2.txt

      Creates said files as empty files in the current dir., if 
      files by that name don't yet exist yet; otherwise sets their
      LastWriteTime to now. Note the use of spaces (only) to
      separate multiple file names.

    touch *.txt

      Sets the LastWriteTime of all matching files, if any, to now.

    Note:
     * No parameter (names) other than -? are supported; all other
       arguments are interpreted as file names. 
     * If you have WSL installed, you can alternatively call
       `wsl touch ...`, which provides access to all features of the
        Unix utility.            
    #>

    # As a courtesy, interpret -h, --help or the absence of arguments the same as -?
    if (-not $args -or $args[0] -in '-h', '--help') { return touch -? }

    # Get any existing files...
    $item = Get-Item -ea SilentlyContinue -ErrorVariable errs -Path $args
    if ($item) { 
      # ... and update their timestamps.
      Write-Verbose "Setting LastWriteTime timestamps to now for: $item"
      $item.ForEach('LastWriteTime', [datetime]::now)
    }
    # Create any files that couldn't be found.
    $errs | ForEach-Object {
      if ($_.Exception -is [System.Management.Automation.ItemNotFoundException]) {
        Write-Verbose "Creating file: $_.TargetObject"
        $null = New-Item -Type File -Path $_.TargetObject
      }
      else {
        # Unexpected error, pass it through.
        $_ | Write-Error
      }
    }
  }
}
Was this page helpful?
0 / 5 - 0 ratings