Private/Powershell/PsFileUtil.ps1

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
<############################################################################
 # Like "Out-File" but always output UTF 8 not unicode and don't include UTF BOM marker
 # copied from https://stackoverflow.com/questions/5596982/using-powershell-to-write-a-file-in-utf-8-without-the-bom
 ############################################################################>

<#
.SYNOPSIS
  Outputs to a UTF-8-encoded file *without a BOM* (byte-order mark).

.DESCRIPTION
  Mimics the most important aspects of Out-File:
  * Input objects are sent to Out-String first.
  * -Append allows you to append to an existing file, -NoClobber prevents
    overwriting of an existing file.
  * -Width allows you to specify the line width for the text representations
     of input objects that aren't strings.
  However, it is not a complete implementation of all Out-String parameters:
  * Only a literal output path is supported, and only as a parameter.
  * -Force is not supported.

  Caveat: *All* pipeline input is buffered before writing output starts,
          but the string representations are generated and written to the target
          file one by one.

.NOTES
  The raison d'�tre for this advanced function is that, as of PowerShell v5,
  Out-File still lacks the ability to write UTF-8 files without a BOM:
  using -Encoding UTF8 invariably prepends a BOM.

#>

Function Out-FileUtf8NoBom {

  [CmdletBinding()]
  param(
    [Parameter(Mandatory, Position=0)] [string] $LiteralPath,
    [switch] $Append,
    [switch] $NoClobber,
    [AllowNull()] [int] $Width,
    [Parameter(ValueFromPipeline)] $InputObject
  )

  #requires -version 3

  # Make sure that the .NET framework sees the same working dir. as PS
  # and resolve the input path to a full path.
  [System.IO.Directory]::SetCurrentDirectory($PWD) # Caveat: .NET Core doesn't support [Environment]::CurrentDirectory
  $LiteralPath = [IO.Path]::GetFullPath($LiteralPath)

  # If -NoClobber was specified, throw an exception if the target file already
  # exists.
  if ($NoClobber -and (Test-Path $LiteralPath)) {
    Throw [IO.IOException] "The file '$LiteralPath' already exists."
  }

  # Create a StreamWriter object.
  # Note that we take advantage of the fact that the StreamWriter class by default:
  # - uses UTF-8 encoding
  # - without a BOM.
  $sw = New-Object IO.StreamWriter $LiteralPath, $Append

  $htOutStringArgs = @{}
  if ($Width) {
    $htOutStringArgs += @{ Width = $Width }
  }

  # Note: By not using begin / process / end blocks, we're effectively running
  # in the end block, which means that all pipeline input has already
  # been collected in automatic variable $Input.
  # We must use this approach, because using | Out-String individually
  # in each iteration of a process block would format each input object
  # with an indvidual header.
  try {
    $Input | Out-String -Stream @htOutStringArgs | % { $sw.WriteLine($_) }
  } finally {
    $sw.Dispose()
  }

}

<#
 # from https://stackoverflow.com/questions/34559553/create-a-temporary-directory-in-powershell
 #>

Function New-TemporaryDirectory {
    $parent = [System.IO.Path]::GetTempPath()
    $name = [System.IO.Path]::GetRandomFileName()
    $d = New-Item -ItemType Directory -Path (Join-Path $parent $name)
    [string]$path = $d
    return $path
}

<#
.SYNOPSIS
    Get full path of file name from current directory of if only one match found in subdirectories
.DESCRIPTION
    Will attempt to delete existing directory or halt if deletion fails.
.PARAMETER file
    Input string to perform actions on. Also supports pipeline input. Can either be a simple string,
    string with embedded newlines, array of strings, or array of strings some of which have embedded newlines.
    Will handle UNIX or Windows style newlines. If specified do not specify -file parameter.
.PARAMETER here
    Directory to check, defaults to current directory
.PARAMETER ignoreDir
    Ignore any directories matching this array, defaults to "node_modules",".git"
.EXAMPLE
.NOTES
    Author: Brian Woelfel
    Date: 2017/10/17
#>

Function Find-FileFromHere {
    Param(
        [Parameter(Mandatory=$true, Position=1)]
        [string]$file,

        [string]$here = (Get-Location).Path,
        
        [string[]]$ignoreDir = ("node_modules",".git")
    )

    [string]$result = $null
    try {
        if(-not [string]::IsNullOrWhitespace($here)) {
            pushd $here
        }

        if([string]::IsNullOrWhitespace($file)) {
            throw "Missing required parameter -file"
        }

        if([System.IO.Path]::GetFileName($file) -eq $file) {
            # $file does not have any path, get a recursive list of all directories that contain that base file
            # ignoring files in directories with names matching $ignoreDir
            [string[]]$subdirs = ( `
                    # Explantion of the split-path stuff https://stackoverflow.com/questions/10317335/getting-just-the-lowest-level-directory-name-for-a-file-from-split-path-using-po
                    Get-ChildItem -Recurse -File `
                        | Where-Object { $_.Name -eq $file } `
                        | Where-Object { -not $ignoreDir.Contains( ( Split-Path $_.Directory -Leaf ) ) } `
                        | Select-Object -Property Directory `
                        | ForEach-Object { Write-Output "$($_.Directory)" } 
` )

            if($subdirs.Count -eq 1) {
                # No path specified, and exactly one matching file found in current or child directories, that's it
                $result = "$($subdirs[0])\$($file)"
            } elseif($subdirs.Count -eq 0) {
                throw "Cannot find any files matching '$($file)' in directory '$here'"
            } else {
                $msg = ($subdirs -join "; ")
                throw "Found file '$($file)' in more than one directory at '$here', including: $msg"
            }
        } else {
            # $file does have path, don't bother searching
            $result = $file
        }

        if(-not (Test-Path $result)) {
            throw "Expected file '$result' does not exist."
        }
    } finally {
        if(-not [string]::IsNullOrWhitespace($here)) {
            popd
        }
    }
    return $result

}