Xavier Peña
Xavier Peña

Reputation: 7909

Apply setCaseSensitiveInfo recursively to all folders and subfolders

I am trying to configure my dotnet core project (in Windows) as "case sensitive", so it behaves as in my production server (linux).

I have found this way of doing it:

fsutil.exe file setCaseSensitiveInfo "C:\my folder" enable

The problem is that this function is not recursive:

The case sensitivity flag only affects the specific folder to which you apply it. It isn’t automatically inherited by that folder’s subfolders.

So I am trying to build a powershell script that applies this to all folders and subfolders, recursively.

I have tried googling something similar and just modifying the command line, but I don't seem to find the corrent keywords. This is the closest that I've gotten to this sort of example.

Upvotes: 44

Views: 13957

Answers (7)

Michael Adams
Michael Adams

Reputation: 1

As fsutil can no longer change the case-sensitivity of any but empty directories, I created a powershell function which steps down a directory tree recursively, it renames directories temporarily, creates a new empty one and sets it to be Case-Sensitive with fsutil, then moves all the content across and deletes the old directory. In my case I needed to take ownership and give full permissions first, so the script below has optional lines to do each of those things.

#!"C:\Program Files\PowerShell\7\pwsh.EXE"
# Make-CaseSensitive-InPlace.ps1
param ( [string]$targetdir )

# uncomment if we need to recurively take ownership of the directory
# takeown /F "$targetdir" /R /D Y /SKIPSL >$null

# uncomment if we need to give ourselves full permissions of the dir tree
# icacls "$targetdir" /grant:r ${env:USERNAME}:F /T /L /C /Q


# Make directory subtree case-sensitive (even not empty) by moving data to new sensitive dirs
function Move-to-CaseSensitive {
    param ( [string]$_path )

    # Define dirs
    $_dirs = @(Get-Item -Path $_path).FullName + (Get-ChildItem -Path $_path -Recurse -Directory).FullName

    # Loop through each, rename, create new case sensitive, move contents, remove
    foreach ($_dir in $_dirs) {
        # Rename orig -> temp, create new empty orig
        Rename-Item -Path $_dir -NewName "${_dir}__"
        New-Item -Path $_dir -ItemType Directory

        # fsutil.exe file... works at commadline, not in script without encapsulatio of parameters
        Start-Process -FilePath "fsutil.exe" -ArgumentList "file SetCaseSensitiveInfo $_dir enable" `
          -Wait -NoNewWindow -RedirectStandardOutput "${env:Temp}\null.txt"

        # Move contents from the renamed folder & then remove empty folder
        Get-ChildItem -Path "${_dir}__" -Force | Move-Item -Destination $_dir
        # better check _dir is empty before deletion
        if (Get-ChildItem -path "${_dir}__") {
            Write-Host "${_dir}__ not empty. Exiting."
            exit
        } else {
            Remove-Item -Path "${_dir}__"
        }
    }
}

Move-to-CaseSensitive $targetdir

Upvotes: 0

olavrb
olavrb

Reputation: 11

As others have stated: Enabling case sensitivity on directories with content no longer works.

And contrary to what others have stated, moving directories and files back to a parent directory where you enabled case sensitivity, does not enable case sensitivity for those directories. One could maybe script a move of files where you recreate directories, then move files to it.

In my case, I wanted to enable case sensitivity on my git directory, where all sub directories are located in some managed git instance (github.com etc.).

I ended up deleting all content inside my git folder, enable case sensitivty for it, then clone down repositories again. Be sure to have pushed all changes from local git before doing this.

I created a PowerShell script to query, enable or disable case sensitivity. I'll share it here in case it's useful for someone.

<#
    .NOTES
        # Script info
        Author:   Olav Rønnestad Birkleand | github.com/o-l-a-v
        Created:  2024-06-22
        Modified: 2024-06-22

        # Learnings
        * Directory must be empty before enabling case sensitivity.
          * Example: `cmd /c 'fsutil.exe file setCaseSensitiveInfo "C:\Users\olavb\Git" enable'`
          * Results: `Error:  The directory is not empty.`
          * Fix: Delete all inside "git" folder, enable case sensitivity for "git" folder, clone repos again.
        * Can set git config per repo to not ignore case sensitivity
          * `git config core.ignorecase false`

        # Resources
        * https://learn.microsoft.com/en-us/windows/wsl/case-sensitivity
        * 2018-07-30 "Apply setCaseSensitiveInfo recursively to all folders and subfolders"
        https://stackoverflow.com/questions/51591091
        * 2018-11-20 "How to enable NTFS support to treat folders as case sensitive on Windows 10"
        https://www.windowscentral.com/how-enable-ntfs-treat-folders-case-sensitive-windows-10
        * 2018-02-28 "Per-directory case sensitivity and WSL"
        https://devblogs.microsoft.com/commandline/per-directory-case-sensitivity-and-wsl/

    .EXAMPLE
        & $(Try{$psEditor.GetEditorContext().CurrentFile.Path}Catch{$psISE.CurrentFile.FullPath}) `
            -ParentDirectory ('{0}\Git' -f $env:USERPROFILE) -Operation 'Query'
#>


# Input and expected output
[OutputType([System.Void])]
Param(
    [Parameter(Mandatory)]
    [ValidateScript({[System.IO.Directory]::Exists($_)})]
    [string] $ParentDirectory,

    [Parameter(Mandatory)]
    [ValidateSet('Enable','Disable','Query')]
    [string] $Operation
)


# PowerShell preferences
$ErrorActionPreference = 'Stop'
$InformationPreference = 'Continue'


# Failproof
## Must run as administrator if changing anything
if (
    $Operation -ne 'Query' -and -not
    ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
) {
    Throw 'Must run as admin to change case sensitivity.'
}


# Get all child directories
$ChildDirectories = [string[]](
    [System.IO.Directory]::GetDirectories(
        $ParentDirectory,
        '*',
        [System.IO.SearchOption]::AllDirectories
    )
)
Write-Information -MessageData ('Parent dir "{0}" has "{1}" child dirs.' -f $ParentDirectory, $ChildDirectories.'Count'.ToString())


# Gather all directories
$Directories = [string[]]($([string[]]($ParentDirectory) + $ChildDirectories))


# Query, will use the results for enable/disable too
## Introduce step
Write-Information -MessageData 'Querying for current status.'

## Query
$Directories = [PSCustomObject[]](
    $Directories.ForEach{
        [PSCustomObject]@{
            'Path'     = [string] $_
            'Children' = [uint16]([System.IO.Directory]::GetDirectories($_).'Count' + [System.IO.Directory]::GetFiles($_).'Count')
            'Result'   = [string](cmd /c ('fsutil.exe file queryCaseSensitiveInfo "{0}"' -f $_))
        }
    }
)

## Parse out status from query result
$Directories.ForEach{
    $null = Add-Member -InputObject $_ -MemberType 'NoteProperty' -Force -Name 'Status' -Value (
        $_.'Result'.Split(' ')[-1].TrimEnd('.')
    )
}

## Output info on the parent directory
Write-Information -MessageData 'Parent directory'
$Directories[0] | Format-List

## Output statistics for all directories
Write-Information -MessageData ('Statistics for all "{0}" directories.' -f $Directories.'Count'.ToString())
$Directories.'Status' | Group-Object -NoElement | Format-Table -AutoSize


# Enable or disable
if ($Operation -in 'Enable','Disable') {
    Write-Information -MessageData 'Can only enable/disable for empty directories.'
    $DirectoriesCanBeChanged = [string[]](
        $Directories.Where{
            $_.'Children' -le 0 -and
            -not $_.'Status'.StartsWith($Operation,'OrdinalIgnoreCase')
        }.'Path'
    )
    Write-Information -MessageData ('Can change case sensitivity for "{0}" directories.' -f $DirectoriesCanBeChanged.'Count'.ToString())
    if ($DirectoriesCanBeChanged.'Count' -gt 0) {
        # Change case sensitivity
        $DirectoriesCanBeChanged = [PSCustomObject[]](
            $DirectoriesCanBeChanged.ForEach{
                [PSCustomObject]@{
                    'Path'   = [string] $_
                    'Result' = cmd /c ('fsutil.exe file setCaseSensitiveInfo "{0}" {1}' -f $_, $Operation)
                }
            }
        )
        # Output grouped / summarized results
        $DirectoriesCanBeChanged.'Result'.ForEach{
            if ($_.StartsWith('Error','OrdinalIgnoreCase')) {
                $_
            }
            else {
                'Success'
            }
        } | Group-Object -NoElement | Format-Table -AutoSize
    }
}

Upvotes: 1

vincenzoml
vincenzoml

Reputation: 433

On windows 11, the other answers are not correct, as fsutil requires that the directory is not empty. To overcome this, I created a NEW empty directory, used fsutil file setCaseSensitiveInfo to set the case sensitive flag on the new directory, then MOVED the files from the other directory inside the new one. This works, as the directories are re-created when moved, and new directories inherit the case sensitive flag.

Upvotes: 4

Fede Garcia
Fede Garcia

Reputation: 807

In my case I had to first enable the Linux subsystem before using the fsutil tool. So my steps were:

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

then restart, and then @robdy 's solution:

(Get-ChildItem -Recurse -Directory).FullName | ForEach-Object {fsutil.exe file setCaseSensitiveInfo $_ enable}

Upvotes: 4

Robert Dyjas
Robert Dyjas

Reputation: 5227

Correct code:

(Get-ChildItem -Recurse -Directory).FullName | ForEach-Object {fsutil.exe file setCaseSensitiveInfo $_ enable}

Explanation:

NOTE: The code in the answer assumes you're in the root of the directory tree and you want to run fsutil.exe against all the folders inside, as it's been pointed out in the comments (thanks @Abhishek Anand!)

Get-ChildItem -Recurse -Directory will give you list of all folders (recursively).

As you want to pass their full path, you can access it by using .FullName[1] (or more self-explanatory | Select-Object -ExpandProperty FullName ).

Then you use ForEach-Object to run fsutil.exe multiple times. Current file's FullName can be accessed using $_ (this represents current object in ForEach-Object)[2].

Hint:

If you want more tracking of what's currently being processed you can add the following to write the path of currently processed file to the console: ; Write-Host $_ (semicolon ; is to separate from fsutil invocation) as it was pointed out in the comments (thanks Fund Monica's Lawsuit !)


[1] .FullName notation works for PowerShell 3.0 and greater, Select-Object -ExpandProperty FullName is preferred if there's a chance that lower version will be used.

[2] $_ is an alias for $PSItem

Upvotes: 63

jkeatley
jkeatley

Reputation: 91

With Cygwin and bash shell, you can do this:

$ find $THEDIR -type d -exec fsutil file setCaseSensitiveInfo "{}" enable \;

It appears that Windows handles the '/' characters output by the find command just fine.

Upvotes: 5

Daniel Smedema
Daniel Smedema

Reputation: 969

(Get-ChildItem -Recurse -Directory).FullName | ForEach-Object {if (-Not ($_ -like '*node_modules*')) { fsutil.exe file setCaseSensitiveInfo $_ enable } }

I modified @robdy's code to allow excluding node_modules. You can replace the "node_modules" bit in the above with anything to exclude filepaths containing it.

If you're working with npm, you probably want to exclude node_modules. @robdy's answer is great, but was taking minutes at a time iterating over every single node package folder even if I didn't have the package installed; given that this is something one might want to run fairly often since directories might be added all the time, and since you probably aren't modifying anything in node_modules, excluding it seems reasonable.

Upvotes: 6

Related Questions