├── .gitignore ├── LICENSE ├── README.md └── pint.cmd /.gitignore: -------------------------------------------------------------------------------- 1 | apps 2 | deps 3 | dist 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dzmitry Vensko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pint 2 | Portable INsTaller - a command line manager of portable applications for Windows, which fits into a single file. 3 | [Support forum](https://www.portablefreeware.com/forums/viewtopic.php?f=6&t=22888) at TPFC. 4 | 5 | Pint is a tool for the people who prefer unpacking over installing. Its primary goal was to provide a way to easily manage a collection of portable apps. With the emergence of portabilizers like [yaP](http://rolandtoth.hu/yaP/), [PortableApps.com Platform](http://portableapps.com/platform/features) and other, focusing solely on the natively portable apps became irrelevant. Pint downloads and unpacks everything it can. At the moment it supports: 6 | - Zip archives. 7 | - MSI packages. 8 | - All formats supported by 7-zip (7z, RAR, NSIS installers, etc.). 9 | - Inno Setup installers. 10 | 11 | # Features 12 | - Downloads, unpacks and removes applications. 13 | - Checks for updates and downloads them if available. Unlike Chocolatey and Scoop, Pint's databases do not require constant attention by humans. Pint will automatically detect, download and install an update once it becomes available on a website. 14 | - Extracts download links from websites using [Xidel](http://www.videlibri.de/xidel.html). 15 | - Supports RSS and PAD files as link sources. 16 | - Unpacks various types of archives and installers and upgrades apps, keeping configuration files intact. 17 | - Apps can be installed into arbitrary subdirectories under *apps*. This allows to keep yaP and PortableApps.com packages up to date. 18 | - Automatically detects console applications and creates shim files for them in the *shims* directory. 19 | - Can remember, if a 32-bit or a 64-bit application was installed. 20 | - Can handle multiple installations of the same application. 21 | - Detects app versions. 22 | - Forms a report with installed applications. 23 | - Can temporarily suppress updates for selected apps. 24 | - Can update itself. 25 | - Can use multiple local and remote databases, even choose not to use default ones. 26 | - Allows to override paths and settings via environment variables. 27 | 28 | # What Pint is not 29 | - **Pint is not a portabilizer**, though it provides ways to manage portable apps more easily. 30 | - Pint can't install a particular version of an app, only the latest, preferably portable, one. Though, it's often able to detect a version after installation. 31 | 32 | # Installation 33 | To install Pint, save [pint.cmd](https://github.com/vensko/pint/raw/master/pint.cmd) to a separate directory. By default, Pint will create the following items: 34 | - **apps** *a directory for your apps* 35 | - **apps\\.shims** *a directory for shims* 36 | - **dist** *a directory for downloaded archives and installers* 37 | - **deps** *a directory for Pint's dependencies* 38 | 39 | All paths are customisable, see the [Environment variables](https://github.com/vensko/pint/wiki/Environment-Variables) chapter. 40 | 41 | # Requirements 42 | - Powershell 2.0+ 43 | - .NET Framework 2.0+ 44 | 45 | Both are shipped with Windows 7+. 46 | 47 | There are also hard dependencies, installed automatically when needed: 48 | - [7-zip](http://www.7-zip.org/) - file archiver supporting a wide range of formats, 49 | - [Xidel](http://www.videlibri.de/xidel.html) - HTML/XML/JSON data extraction tool, 50 | - [innoextract](http://constexpr.org/innoextract/) - unpacks installers created by Inno Setup, 51 | - [shimgen](https://github.com/chocolatey/choco/blob/master/src/chocolatey.resources/tools/shimgen.exe) - shim generator by Chocolatey team. 52 | 53 | # Usage 54 | ``` 55 | pint 56 | ``` 57 | 58 | ## Available commands 59 | 60 | ### `pint self-update` 61 | Self-explanatory. Updates Pint to the latest version. 62 | 63 | ### `pint search []` 64 | If `` is empty, yields a full list of app IDs from all databases. 65 | If not, searches the databases for ``. 66 | 67 | Example: `pint search xnview` 68 | 69 | ### `pint download []` 70 | `` is an ID from the `search` list. This downloads one or more apps into **dist** without unpacking them. All downloaded packages are stored with filenames in the format `----`. 71 | 72 | Keep in mind, that the architecture attribute in Pint never refers to an actual bit count, but rather to a *preferred* value. If a 64-bit version of an app is not available yet and your processor is 64-bit, a 32-bit version will be downloaded and marked as 64. With a 64-bit version released, the app will be automatically upgraded from 32 to 64 bit. 73 | 74 | Example: `pint download xnview foobar2000` 75 | 76 | ### `pint install []` 77 | Downloads an archive (or a few) into **dist** and unpacks them into subdirectories with corresponding names under **apps**. 78 | 79 | Example: `pint install foobar2000` 80 | 81 | ### `pint installto [32|64]` 82 | Installs `` into an arbitrary **apps** subdirectory. After installation, the app directory can be renamed or moved anywhere under **apps**, all installations are self-contained. Check `pint l` for a changed `` value. 83 | 84 | Optionally, preferred bit count can be set with the third parameter (useful, if you need to force installation of a 32-bit version in a 64-bit system). 85 | 86 | Example: `pint installto subtitle-workshop "Subtitle Workshop"` 87 | For more examples, see [this chapter](#custom-install-destinations-installto). 88 | 89 | ### `pint list` 90 | Shows a full list of installed apps with some metadata. 91 | 92 | ### `pint l` 93 | Lists only directories without retrieving metadata. 94 | If the `pint list` table becomes too large, this may be a faster way to check directory names. 95 | 96 | ### `pint reinstall []` 97 | Forces reinstallation of the apps. 98 | 99 | Example: `pint reinstall foobar2000 "Subtitle Workshop"` 100 | 101 | ### `pint remove []` 102 | Removes the subdirectories. This is fully equivalent to manual deletion of the folders. 103 | 104 | Example: `pint remove "Subtitle Workshop"` 105 | 106 | ### `pint purge []` 107 | Removes subdirectories AND corresponding archives from **dist**. 108 | 109 | Example: `pint purge foobar2000 1by1` 110 | 111 | ### `pint cleanup` 112 | Deletes all downloaded installers and archives from **dist**. 113 | 114 | ### `pint outdated [ []]` 115 | Checks for updates for the apps. With parameters omitted, Pint will check all installed apps. 116 | 117 | Example: `pint outdated 7-zip` 118 | 119 | ### `pint upgrade [ []]` 120 | Checks for updates AND installs them if available. Same here, without parameters this will try to upgrade everything. 121 | 122 | Example: `pint upgrade foobar2000 1by1 7-zip` 123 | 124 | ### `pint forget []` 125 | Pint never touches subdirectories, where it hadn't installed anything previously. Subdirectories with manually installed apps will simply be ignored. This command removes Pint's metadata from the subdirectories. To make them manageable again, use `installto`. 126 | 127 | Example: `pint forget 7-zip` 128 | 129 | ### `pint pin []` 130 | Keeps Pint's metadata yet suppresses automatic updates for the apps. 131 | 132 | Example: `pint pin 7-zip` 133 | 134 | ### `pint unpin []` 135 | Allows automatic updates (undoes `pin`). 136 | 137 | Example: `pint unpin 7-zip` 138 | 139 | ### `pint shims` 140 | Removes all shims files and recreates them. 141 | 142 | ### `pint test [|] [32|64]` 143 | Tests given file, URL or app ID. Verifies remote file availability, content type and reported content length. 144 | 145 | Examples: 146 | `pint test "D:\my-packages.ini"` 147 | `pint test foobar2000` 148 | 149 | ### `pint info ` 150 | Show package configuration. 151 | 152 | ### `pint unpack ` 153 | Unpacks a file to a specified directory. 154 | 155 | Example: `pint unpack "D:\foobar2000.zip" "D:\foobar2000"` 156 | 157 | # Custom install destinations (installto) 158 | Pint deals with app identifiers only during their download and/or installation. After that, all commands refer to actual subdirectories in **apps**, e.g.: 159 | - apps\\**firefox** 160 | - apps\\**foobar2000** 161 | 162 | To keep things simple, you may use only the `install` command. This way, database identifiers and subdirectories will always be the same. But if you prefer storing your browser in *apps\Mozilla Firefox* instead of *apps\firefox*, this can be done with `installto`: 163 | ``` 164 | pint installto firefox "Mozilla Firefox" 165 | ``` 166 | FF will be installed into 167 | - apps\\**Mozilla Firefox** 168 | 169 | From this point, it will have to be referred to as "Mozilla Firefox": 170 | ``` 171 | pint outdated "Mozilla Firefox" 172 | pint remove "Mozilla Firefox" 173 | ``` 174 | 175 | For another example, consider a yaP setup with the directory structure: 176 | - apps\WinRAR\WinRARPortable.exe (yaP executable) 177 | - apps\WinRAR\x86\ 178 | - apps\WinRAR\x64\ 179 | 180 | To be able to manage this setup, run this: 181 | ``` 182 | pint installto winrar WinRAR\x86 32 183 | pint installto winrar WinRAR\x64 64 184 | ``` 185 | Pint will handle both copies and update them using a correct archive. 186 | As can be seen via the `list` command, they'll be referred to as *WinRAR\x86* and *WinRAR\x64* respectively: 187 | ``` 188 | pint pin WinRAR\x86 189 | pint upgrade WinRAR\x64 190 | ``` 191 | 192 | Absolute paths outside **apps** are allowed. They will not be visible in `list` and not automatically included by `upgrade` or `outdated`, because there is no database tracking their locations. To manage them, you'll always have to use absolute paths, e.g. 193 | ``` 194 | pint installto imagine "E:\Total Commander\Plugins\Imagine" 195 | pint upgrade "E:\Total Commander\Plugins\Imagine" 196 | ``` 197 | 198 | # More 199 | - [Environment Variables](https://github.com/vensko/pint/wiki/Environment-Variables) 200 | - [Database: How To](https://github.com/vensko/pint/wiki/Database-How-To) 201 | - [Database: Repository](https://github.com/vensko/pint-packages) 202 | 203 | # Alternatives 204 | - [Scoop](https://github.com/lukesampson/scoop) 205 | - [Chocolatey](https://github.com/chocolatey/choco) 206 | -------------------------------------------------------------------------------- /pint.cmd: -------------------------------------------------------------------------------- 1 | <# : 2 | @echo off 3 | @setlocal 4 | 5 | rem PINT - Portable INsTaller 6 | rem https://github.com/vensko/pint 7 | 8 | set "PINT=%~f0" 9 | set "PINT_SELF_URL=https://raw.githubusercontent.com/vensko/pint/master/pint.cmd" 10 | set "PINT_CURRENT_DIR=%cd%" 11 | 12 | rem Set variables if they weren't overriden 13 | if not defined PINT_APP_DIR set "PINT_APP_DIR=%~dp0apps" 14 | if not defined PINT_DIST_DIR set "PINT_DIST_DIR=%~dp0dist" 15 | if not defined PINT_DEPS_DIR set "PINT_DEPS_DIR=%~dp0deps" 16 | if not defined PINT_SHIM_DIR set "PINT_SHIM_DIR=%PINT_APP_DIR%\.shims" 17 | if not defined PINT_USER_AGENT set "PINT_USER_AGENT=PintBot/1.0 (+https://github.com/vensko/pint)" 18 | if not defined PINT_PACKAGES_DIR set "PINT_PACKAGES_DIR=%~dp0packages" 19 | if not defined PINT_DB set "PINT_DB=https://d.vensko.net/pint/db/packages.ini,%~dp0packages.user.ini" 20 | if not defined PINT_CACHE_TTL set "PINT_CACHE_TTL=24" 21 | 22 | rem Start 64bit PowerShell even from 32bit command line 23 | set "POWERSHELL=%SystemRoot%\sysnative\windowspowershell\v1.0\powershell.exe" 24 | if not exist "%POWERSHELL%" set "POWERSHELL=powershell" 25 | 26 | set "_args=%*" 27 | if defined _args set "_args=%_args:"=""""""%" 28 | %POWERSHELL% -NoLogo -NoProfile -executionpolicy bypass "$s = ${%PINT%} | out-string; $s += """pint-start %_args%"""; iex($s)" || exit /b 1 29 | exit /b 0 30 | 31 | end Batch / begin PowerShell #> 32 | 33 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 34 | [Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} 35 | 36 | $global:httpMaxRedirects = 5 37 | $global:httpTimeout = 15000 38 | $global:arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'x86') {32} else {64} 39 | $global:db = '' 40 | 41 | $global:dependencies = @" 42 | [xidel] 43 | dist = https://master.dl.sourceforge.net/project/videlibri/Xidel/Xidel%200.9.6/xidel-0.9.6.win32.zip 44 | [innoextract] 45 | dist = https://github.com/dscharrer/innoextract/releases/latest 46 | link = win, .zip 47 | [7z] 48 | dist = http://www.7-zip.org/download.html 49 | link = .msi, !x64 50 | only = 7z.exe, 7z.dll 51 | [shimgen] 52 | dist = https://github.com/chocolatey/choco/raw/master/src/chocolatey.resources/tools/shimgen.exe 53 | type = standalone 54 | "@ + "`n" 55 | 56 | function is-file([string]$p) 57 | { 58 | test-path $p -pathtype leaf 59 | } 60 | 61 | function is-dir([string]$p) 62 | { 63 | test-path $p -pathtype container 64 | } 65 | 66 | function ensure-dir([string]$p) 67 | { 68 | if (!(is-dir $p)) { md $p -ea 1 | out-null } 69 | } 70 | 71 | function clist([string]$str) 72 | { 73 | [array]($str -split ',' |% trim |? {$_}) 74 | } 75 | 76 | function get-pad($list) 77 | { 78 | $max = $list | sort length -desc | select -first 1 79 | $max.length + 2 80 | } 81 | 82 | function string-to-xpath([string]$str, [bool]$rss) 83 | { 84 | $exts = @('.7z', '.zip', '.rar', '.paf.exe') 85 | 86 | ( 87 | clist $str.ToLower() |% { 88 | $p = $_ 89 | $not = ($p[0] -eq '!') 90 | $attr = if ($rss -or ($p[-1] -eq '"')) { '.' } else { '@href' } 91 | $p = $p.trimstart('!').trim('"') 92 | 93 | switch ($p) { 94 | {@('.arch', '.any') -contains $_} { 95 | $e = if ($_ -eq '.any') { $exts + @('.exe') } else { $exts } 96 | $p = $e |% { "contains(lower-case($attr), `"$_`")" } 97 | $p = '(' + ($p -join ' or ') + ')' 98 | break 99 | } 100 | default { 101 | $p = "contains(lower-case($attr), `"$p`")" 102 | } 103 | } 104 | 105 | if ($not) { $p = "not($p)" } 106 | 107 | $p 108 | } 109 | ) -join ' and ' 110 | } 111 | 112 | ### INI parser 113 | 114 | function ini-get-sections([string]$ini, [string]$search) 115 | { 116 | [regex]::Matches($ini, "(?:^|\n)\[(.*?$search.*?)\]", 'IgnoreCase') |% {$_.groups[1].value} | get-unique 117 | } 118 | 119 | function extract-ini-section([string]$ini, [string]$section) 120 | { 121 | $m = [regex]::Matches($ini, "(?:^|\n)\[$section\](((?!\n\[).)+)", 'Singleline,IgnoreCase') 122 | if (!$m.count) { return $null } 123 | parse-ini-section $m[$m.count-1].groups[1].value 124 | } 125 | 126 | function parse-ini-section([string]$ini) 127 | { 128 | $res = @{} 129 | [regex]::Matches($ini, "^\s*(\w+?)\s*=\s*(.+)\s*$", 'm') |% { 130 | $res[$_.groups[1].value] = $_.groups[2].value.trim() 131 | } 132 | $res 133 | } 134 | 135 | ### Databases 136 | 137 | function pint-dblist 138 | { 139 | return clist $env:PINT_DB 140 | } 141 | 142 | function get-remote-db($url) 143 | { 144 | try { 145 | if ($env:PINT_CACHE_TTL -eq 0) { 146 | return get-text $url 147 | } 148 | 149 | $cache = $url -replace '[^\w]', '' 150 | $file = join-path $env:TEMP "pint-cache-$cache.ini" 151 | $timespan = new-timespan -hours $env:PINT_CACHE_TTL 152 | 153 | if ((is-file $file) -and (get-date) - (gi $file).LastWriteTime -lt $timespan) { 154 | return [IO.File]::ReadAllText($file) 155 | } 156 | 157 | $text = get-text $url 158 | $text | out-file $file -encoding ascii 159 | $text 160 | } catch { 161 | write-host $_.Exception.InnerException.Message ' ' $url -f red 162 | return '' 163 | } 164 | } 165 | 166 | function pint-get-db-ini 167 | { 168 | if ($global:db) { 169 | return $global:db 170 | } 171 | 172 | $db = '' 173 | 174 | pint-dblist |% { 175 | $db += "`n" 176 | if (is-file $_) { 177 | $db += [IO.File]::ReadAllText($_) 178 | } elseif ($_.contains('://')) { 179 | $db += get-remote-db $_ 180 | } 181 | } 182 | 183 | ($global:db = $db) 184 | } 185 | 186 | function pint-app-info($app) 187 | { 188 | $info = if (is-file ($file = join-path $env:PINT_PACKAGES_DIR "$app.ini")) { 189 | parse-ini-section ([IO.File]::ReadAllText($file)) 190 | } else { 191 | extract-ini-section (pint-get-db-ini) $app 192 | } 193 | if (!$info -or !$info.keys.count) { 194 | throw "Unable to find '$app' in the database." 195 | } 196 | $info 197 | } 198 | 199 | function pint-app-list 200 | { 201 | $list = @() 202 | $list += dir $env:PINT_PACKAGES_DIR -n '*.ini' -ea 0 |% { [IO.Path]::GetFileNameWithoutExtension($_) } 203 | $list += ini-get-sections (pint-get-db-ini) 204 | $list | sort | get-unique 205 | } 206 | 207 | ### Web requests 208 | 209 | function get-text($src) 210 | { 211 | $client = new-object Net.WebClient 212 | $client.Headers['User-Agent'] = user-agent $src 213 | $client.DownloadString($src) 214 | } 215 | 216 | function user-agent($url) 217 | { 218 | if ($url -match '(dropbox\.com|osdn\.)') { 219 | return 'curl/7.55.0' 220 | } 221 | $env:PINT_USER_AGENT 222 | } 223 | 224 | function pint-make-ftp-request([string]$url, [bool]$download) 225 | { 226 | $req = [Net.WebRequest]::Create($url) 227 | $req.Timeout = $global:httpTimeout 228 | $req.KeepAlive = $false 229 | if (!$download) { $req.Method = [Net.WebRequestMethods+Ftp]::GetFileSize } 230 | $req.GetResponse() 231 | } 232 | 233 | function pint-make-http-request([string]$url, [bool]$download) 234 | { 235 | try { 236 | $req = [Net.WebRequest]::Create($url) 237 | $req.Timeout = $global:httpTimeout 238 | $req.userAgent = user-agent $url 239 | $req.AllowAutoRedirect = $true 240 | $req.KeepAlive = $false 241 | $req.MaximumAutomaticRedirections = $global:httpMaxRedirects 242 | $req.Accept = '*/*' 243 | if (!$url.contains('sourceforge.net') -and !$url.contains('portableapps.com')) { 244 | $req.Referer = $url 245 | } 246 | $req.GetResponse() 247 | } catch [Management.Automation.MethodInvocationException] { 248 | $e = $_.Exception.InnerException 249 | $headers = $e.Response.Headers 250 | 251 | if ($headers -and ([string]$headers['Location']).StartsWith('ftp:')) { 252 | return pint-make-ftp-request $headers['Location'] $download 253 | } 254 | 255 | throw $e 256 | } 257 | } 258 | 259 | function pint-make-request([string]$url, [bool]$download) 260 | { 261 | if ($url.StartsWith('ftp:')) { 262 | pint-make-ftp-request $url $download 263 | } else { 264 | pint-make-http-request $url $download 265 | } 266 | } 267 | 268 | function download-file([Net.WebResponse]$res, [string]$targetFile) 269 | { 270 | ensure-dir (split-path $targetFile) 271 | 272 | $totalLength = [Math]::Floor($res.ContentLength / 1024) 273 | 274 | write-host "Downloading $($res.ResponseUri) ($("{0:N2} MB" -f ($totalLength / 1024)))" 275 | 276 | $remoteName = get-remote-name $res 277 | $rs = $res.GetResponseStream() 278 | $fs = new-object IO.FileStream $targetFile, 'Create' 279 | $buffer = new-object byte[] 512KB 280 | $count = $rs.Read($buffer, 0, $buffer.length) 281 | $downloaded = $count 282 | $progressBar = ($res.ContentLength -gt 1MB) 283 | 284 | while ($count -gt 0) { 285 | $fs.Write($buffer, 0, $count) 286 | $count = $rs.Read($buffer, 0, $buffer.length) 287 | if ($progressBar) { 288 | $downloaded += $count 289 | write-progress -activity "Downloading file $remoteName" -status "Downloaded ($([Math]::Floor($downloaded / 1024))K of $($totalLength)K): " -PercentComplete ((([Math]::Floor($downloaded / 1024)) / $totalLength) * 100) 290 | } 291 | } 292 | 293 | write-progress -completed -activity "Downloading file $remoteName" -status "Done" 294 | 295 | $fs.Flush() 296 | $fs.Close() 297 | 298 | if ($fs.Dispose -ne $null) { 299 | $fs.Dispose() 300 | $rs.Dispose() 301 | } 302 | 303 | $res.Close() 304 | 305 | if ($res.ContentLength -lt 1 -or $res.ContentLength -eq (gi $targetFile).length) { 306 | write-host 'Saved to' $targetFile 307 | $targetFile 308 | } else { 309 | del $targetFile -force 310 | throw "Unable to complete download from $($res.ResponseUri)" 311 | } 312 | } 313 | 314 | function get-remote-name([Net.WebResponse]$res) 315 | { 316 | $name = if (($h = $res.Headers['Content-Disposition']) -and $h.contains('=')) { 317 | ($h -split '=', 2)[1].replace('"', '').trim() 318 | } else { 319 | ($res.ResponseUri -split '/')[-1] 320 | } 321 | 322 | ($name -split '[;:\?]', 2)[0] 323 | } 324 | 325 | ### Dependencies 326 | 327 | function get-dependency([string]$id) 328 | { 329 | if (!(has-dependency $id)) { 330 | write-host "Pint requires $id for this operation, installing automatically..." 331 | $db = $global:db 332 | $global:db = $global:dependencies 333 | pint-force-install $id (join-path $env:PINT_DEPS_DIR $id) 32 334 | $global:db = $db 335 | } 336 | 337 | join-path $env:PINT_DEPS_DIR "$id\$id.exe" 338 | } 339 | 340 | function has-dependency([string]$id) 341 | { 342 | is-file (join-path $env:PINT_DEPS_DIR "$id\$id.exe") 343 | } 344 | 345 | function pint-unpack([string]$file, [string]$dir, [string]$type) 346 | { 347 | $item = gi $file 348 | $file = $item.fullname 349 | write-host 'Unpacking' $item.name 350 | ensure-dir $dir 351 | 352 | switch ($item.extension) { 353 | '.msi' { 354 | & $env:ComSpec /d /c "msiexec /a `"$file`" /norestart /qn TARGETDIR=`"$dir`"" 355 | break 356 | } 357 | {($type -eq 'inno') -or (($_ -eq '.exe') -and (select-string -path $file -pattern 'Inno Setup'))} { 358 | & (get-dependency 'innoextract') -s -c -p -d $dir $file 359 | break 360 | } 361 | {($_ -eq '.zip') -and !(has-dependency '7z')} { 362 | try { 363 | $shell = new-object -com Shell.Application 364 | $zip = $shell.NameSpace($file) 365 | $items = $zip.items() 366 | if ($items.item(0)) { 367 | $shell.Namespace($dir).copyhere($items, 20) 368 | break 369 | } 370 | } catch {} 371 | } 372 | {$true} { 373 | $type = if ($type) {"-t$type"} else {''} 374 | & $env:ComSpec /d /c "`"$(get-dependency '7z')`" x $type -y -bd -bso0 -bsp0 -aoa -o`"$dir`" `"$file`"" 375 | } 376 | } 377 | } 378 | 379 | function pint-get-version([string]$dir) 380 | { 381 | try { 382 | $files = dir $dir -filter *.exe -exclude *portable.exe,uninst*.exe -ea 0 383 | if (!$files) { $files = dir $dir -r -filter *.exe -exclude *portable.exe,uninst*.exe -ea 1 } 384 | $v = ($files | sort length -desc | select -first 1).VersionInfo.ProductVersion.trim() 385 | $v = $v.replace(', ', '.').replace(',', '.') 386 | $v = ($v -split '[- ]+', 2)[0] 387 | if (!($v -match "^[0-9\.]+$")) { return } 388 | while ($v.endswith('.0')) { $v = $v.substring(0, $v.length-2) } 389 | $v 390 | } catch {} 391 | } 392 | 393 | function distdir([string]$file) 394 | { 395 | join-path $env:PINT_DIST_DIR $file 396 | } 397 | 398 | function appdir([string]$path) 399 | { 400 | if (![IO.Path]::isPathRooted($path)) { 401 | if ($path.StartsWith('.')) { 402 | $path = (gi (join-path $env:PINT_CURRENT_DIR $path)).fullname 403 | } else { 404 | $path = join-path $env:PINT_APP_DIR $path 405 | } 406 | } 407 | $path 408 | } 409 | 410 | function get-pint([string]$dir, [int]$ea = 1) 411 | { 412 | gi ((appdir $dir) + "\*.pint") -force -ea $ea 413 | } 414 | 415 | function pint-get-installed-app([string]$p) 416 | { 417 | $app = @{ 418 | dir = appdir $p 419 | arch = $global:arch 420 | pinned = $false 421 | version = "" 422 | size = 0 423 | } 424 | 425 | $file = get-pint $app.dir 0 426 | if (!$file) { return } 427 | 428 | $app.id, $a = $file.basename.trim() -split '[ ]+' 429 | 430 | $a |% { 431 | switch ($_) { 432 | 'pinned' { $app[$_] = $true } 433 | {@(32,64) -contains $_} { $app.arch = $_ } 434 | {$_ -match "^v[\d\.]+$"} { $app.version = $_.substring(1) } 435 | {$_ -match "^\d+$"} { $app.size = [int]$_ } 436 | } 437 | } 438 | 439 | $app 440 | } 441 | 442 | function pint-get-app-meta([string]$id, [string]$arch = $global:arch) 443 | { 444 | $ini = pint-app-info $id 445 | 446 | $res = @{} 447 | $ini.keys | sort |% { 448 | if ($_.endswith(64)) { 449 | if ($arch -eq 64) { 450 | $res[$_.substring(0, $_.length-2)] = $ini[$_] 451 | } 452 | } else { 453 | $res[$_] = $ini[$_] 454 | } 455 | } 456 | 457 | if (!$res.dist) { 458 | throw "No 'dist' key found in '$id' metadata." 459 | } 460 | 461 | $res 462 | } 463 | 464 | function get-dist-link([Hashtable]$meta, [bool]$verbose) 465 | { 466 | $dist = $meta.dist 467 | $link = $meta.link 468 | $follow = $meta.follow 469 | 470 | if (!$link) { return $dist } 471 | 472 | $rss = $dist.contains('/rss?') 473 | 474 | if (!$link.contains('$json') -and !($link.contains('json('))) { 475 | if (!$link.contains('//')) { 476 | $link = string-to-xpath $link $rss 477 | $link = if ($rss) {"//link[$link]"} else {"//a[$link]"} 478 | } 479 | 480 | if ($link.contains('/a')) { 481 | $link += '/resolve-uri(normalize-space(@href), base-uri())' 482 | } 483 | } 484 | 485 | $link = $link.replace('"', "\`"") 486 | 487 | if ($follow) { 488 | if (!$follow.contains('//')) { 489 | $follow = ($follow -split '\|' |% { 490 | '--follow "(//a[' + (string-to-xpath $_).replace('"', "\`"") + '])[1]"' 491 | }) -join ' ' 492 | } else { 493 | $follow = $follow.replace('"', "\`"") -replace '\s*\|\s*','" --follow "' 494 | $follow = " --follow `"$follow`"" 495 | } 496 | } 497 | 498 | if ($verbose) { 499 | write-host 'Extracting download link from' $dist 500 | $out = '' 501 | } else { 502 | $silent = '--silent' 503 | $out = '2>nul' 504 | } 505 | 506 | $method = if ($meta.method) {'-d "'+$meta.data+'" --method '+$meta.method} else {''} 507 | 508 | $proxy = '' 509 | $proxyConfig = get-itemproperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' 510 | if ($proxyConfig.ProxyEnable) { 511 | $proxyAddr = $proxyConfig.ProxyServer -replace "^http://", "" 512 | $proxy = "--proxy=`"$proxyAddr`"" 513 | } 514 | 515 | $xidel = get-dependency 'xidel' 516 | $dist = & $env:ComSpec /d /c "$out `"$xidel`" $method $proxy --header=`"Referer: $dist`" --user-agent=`"$($env:PINT_USER_AGENT)`" `"$dist`" $follow $silent --extract `"($link)[1]`"" 517 | 518 | if ($lastexitcode -or !$dist -or !$dist.contains('://')) { 519 | $dist = $null 520 | } else { 521 | $dist = $dist.trim() 522 | } 523 | 524 | if (!$dist) { 525 | throw "Unable to extract the link from $($meta.dist)" 526 | } 527 | 528 | $dist 529 | } 530 | 531 | function make-app-request([string]$id, [string]$arch, [bool]$download, [bool]$verbose) 532 | { 533 | $meta = pint-get-app-meta $id $arch 534 | $url = if ($meta.link) { get-dist-link $meta $verbose } else { $meta.dist } 535 | $res = pint-make-request $url $download 536 | 537 | if ($res.ContentType.contains('text/html')) { 538 | $res.close() 539 | throw "$url responded with a HTML page." 540 | } 541 | 542 | if (!$download) { 543 | $res.close() 544 | } 545 | 546 | $res 547 | } 548 | 549 | function pint-download-app([string]$id, [string]$arch = $global:arch, $res = $null) 550 | { 551 | if ($res -isnot [Net.WebResponse]) { 552 | $res = make-app-request $id $arch $true $true 553 | } 554 | 555 | $name = get-remote-name $res 556 | 557 | $file = distdir "$id--$arch--$name" 558 | 559 | if ((is-file $file) -and (gi $file).length -eq $res.ContentLength) { 560 | $res.close() 561 | write-host 'The local file has the same size as the remote one, skipping redownloading.' 562 | return $file 563 | } 564 | 565 | download-file $res $file 566 | } 567 | 568 | function pint-force-install([string]$id, [string]$dir, [string]$arch = $global:arch) 569 | { 570 | $file = pint-download-app $id $arch 571 | pint-file-install $id $file $dir $arch 572 | } 573 | 574 | function pint-file-install([string]$id, [string]$file, [string]$destDir, [string]$arch = $global:arch) 575 | { 576 | if (!$destDir) { $destDir = $id } 577 | 578 | $item = gi $file 579 | $destDir = appdir $destDir 580 | $meta = pint-get-app-meta $id $arch 581 | 582 | write-host 'Installing' $id 'to' $destDir 583 | 584 | ensure-dir $destDir 585 | 586 | if ($meta.type -eq 'standalone') { 587 | copy -literalpath $file (join-path $destDir ($id + $item.extension)) -force 588 | } else { 589 | $tempDir = join-path $env:TEMP "pint-$id-$(get-random)" 590 | ensure-dir $tempDir 591 | 592 | if ($meta.args -and $item.extension -eq '.exe') { 593 | $a = '"' + $file + '" ' + $meta.args.replace('$dir', $tempDir) 594 | & $env:ComSpec /d /c "$a" 595 | } else { 596 | pint-unpack $file $tempDir $meta.type 597 | } 598 | 599 | cd -ea stop $tempDir 600 | 601 | $base = if ($meta.base) {$meta.base} else {'.exe'} 602 | 603 | foreach ($p in (dir $pwd -r -n)) { 604 | if ($p.contains($base)) { 605 | cd "$p\.." 606 | break 607 | } 608 | } 609 | 610 | $keep = if ($meta.keep) { 611 | clist $meta.keep 612 | } else { 613 | @('*.ini','*.db') 614 | } 615 | 616 | if ($meta.create) { 617 | $keep += clist $meta.create 618 | } 619 | 620 | $params = @{ 621 | include = $keep 622 | recurse = $true 623 | force = $true 624 | name = $true 625 | ea = 0 626 | } 627 | 628 | dir $destDir @params |% { 629 | $p = join-path $destDir $_ 630 | if (is-dir $p) { 631 | ensure-dir "$pwd\$_" 632 | copy "$p\*" "$pwd\$_" -recurse -force 633 | } else { 634 | ensure-dir (split-path "$pwd\$_") 635 | copy $p "$pwd\$_" -force 636 | } 637 | } 638 | 639 | if ($meta.only) { 640 | $params = @{ 641 | include = clist $meta.only 642 | recurse = $false 643 | force = $true 644 | name = $true 645 | ea = 0 646 | } 647 | 648 | dir $destDir @params |% { del "$destDir\$_" -force -recurse } 649 | 650 | dir $pwd @params |% { 651 | $p = join-path $pwd $_ 652 | if (is-dir $p) { 653 | ensure-dir "$destDir\$_" 654 | copy "$p\*" "$destDir\$_" -recurse -force 655 | } else { 656 | ensure-dir (split-path "$destDir\$_") 657 | copy $p "$destDir\$_" -force 658 | } 659 | } 660 | } else { 661 | $purge = if ($meta.purge -eq 'false') {''} else {'/PURGE'} 662 | $xf = ([string]$meta.xf).replace(',', ' ') + ' *.pint $R0' 663 | $xd = ([string]$meta.xd).replace(',', ' ') + ' $0 $PLUGINSDIR $TEMP $_OUTDIR' 664 | 665 | & $env:COMSPEC /d /c "robocopy `"$pwd`" `"$destDir`" /E /NJS /NJH /NFL /NDL /NC /NP /NS /R:1 /W:1 /XO /FFT $purge /XF $xf /XD $xd" | out-null 666 | 667 | if ($lastexitcode -gt 7) { 668 | write-host "Detected errors while copying from $pwd with Robocopy (code $lastexitcode)." 669 | } 670 | } 671 | 672 | cd $destDir 673 | rd $tempDir -force -recurse 674 | } 675 | 676 | if ($version = pint-get-version $destDir) { 677 | write-host 'Detected version' $version 678 | $version = "v$version" 679 | } 680 | 681 | if ($meta.create) { 682 | clist $meta.create |% { 683 | $newitem = join-path $destDir $_ 684 | if ($newitem.endswith("\")) { 685 | ensure-dir $newitem 686 | } elseif (!(is-file $newitem)) { 687 | ni $newitem -type file -force -ea 0 | out-null 688 | } 689 | } 690 | } 691 | 692 | $pintFile = (@($id, $version, $arch, $item.length) |? {$_}) -join ' ' 693 | $pintFile = join-path $destDir "$pintFile.pint" 694 | 695 | del (join-path $destDir '*.pint') -force 696 | $pintFile = ni $pintFile -type file -force 697 | $pintFile.attributes = 'Hidden' 698 | 699 | if ($destDir.StartsWith($env:PINT_APP_DIR)) { 700 | pint-shims $destDir $meta.shim $meta.noshim | out-null 701 | } 702 | 703 | if ($meta.note) { 704 | write-host $meta.note -f yellow 705 | } 706 | } 707 | 708 | function pint-reinstall 709 | { 710 | if (!$args.count) { 711 | write-host 'Specify a directory to reinstall.' 712 | return 713 | } 714 | 715 | $args |% { 716 | try { 717 | if ($app = pint-get-installed-app $_) { 718 | if ($app.pinned) { 719 | throw "$_ is pinned, use unpin to allow this action." 720 | } 721 | pint-force-install $app.id $app.dir $app.arch 722 | } else { 723 | pint-force-install $_ $_ 724 | } 725 | } catch { 726 | write-warning $_ 727 | } 728 | } 729 | } 730 | 731 | function pint-download 732 | { 733 | if (!$args.count) { 734 | write-warning 'Specify an ID to download.' 735 | return 736 | } 737 | 738 | $args |% { 739 | try { 740 | pint-download-app $_ | out-null 741 | } catch { 742 | write-warning $_ 743 | } 744 | } 745 | } 746 | 747 | function pint-install 748 | { 749 | if (!$args.count) { 750 | write-warning 'Specify an ID to install.' 751 | return 752 | } 753 | 754 | $args |% { pint-installto $_ $_ } 755 | } 756 | 757 | function pint-installto([string]$id, [string]$dir, [string]$arch = $global:arch) 758 | { 759 | try { 760 | if (!$id -or !$dir) { 761 | write-host 'Specify an ID and a destination directory.' 762 | return 763 | } 764 | 765 | if ((dir (appdir $dir) -name -force -ea 0)) { 766 | write-host (appdir $dir) 'is not empty.' 767 | $confirm = read-host -prompt 'Do you want to REPLACE its contents? [Y/N] ' 768 | if ($confirm.trim().ToUpper() -ne 'Y') { return } 769 | } 770 | 771 | pint-force-install $id $dir $arch 772 | } catch { 773 | write-warning $_ 774 | } 775 | } 776 | 777 | function pint-purge 778 | { 779 | if (!$args.count) { 780 | write-warning 'Specify a directory to purge.' 781 | return 782 | } 783 | 784 | $args |% { 785 | if ($app = pint-get-installed-app $_) { 786 | del (distdir "$($app.id)--*.*") -force 787 | } 788 | } 789 | 790 | pint-remove @args 791 | } 792 | 793 | function pint-remove 794 | { 795 | if (!$args.count) { 796 | write-warning 'Specify a directory to remove.' 797 | return 798 | } 799 | 800 | $args |% { 801 | try { 802 | $app = pint-get-installed-app $_ 803 | 804 | if (!$app) { 805 | throw "$_ is not installed." 806 | } 807 | 808 | write-host "Uninstalling $_..." 809 | 810 | $meta = pint-get-app-meta $app.id $app.arch 811 | pint-shims $app.dir $meta.shim $meta.noshim $true 812 | 813 | rd -literalpath $app.dir -recurse -force 814 | write-host $_ 'is removed.' 815 | } catch { 816 | write-warning $_ 817 | } 818 | } 819 | } 820 | 821 | function pint-outdated 822 | { 823 | write-host 'Checking for updates...' 824 | 825 | if (!$args.count) { $args = pint-l } 826 | $pad = get-pad $args 827 | $download = [bool]$global:upgrade 828 | 829 | $args |% { 830 | write-host $_.padright($pad, ' ') -nonewline 831 | 832 | try { 833 | $app = pint-get-installed-app $_ 834 | 835 | if (!$app) { 836 | write-host 'NOT FOUND' -f red 837 | return 838 | } 839 | 840 | if ($download -and $app.pinned) { 841 | write-host 'PINNED' -f yellow 842 | return 843 | } 844 | 845 | if (!$app.size) { 846 | write-host 'NO SIZE DATA' -f darkyellow 847 | return 848 | } 849 | 850 | $res = make-app-request $app.id $app.arch $download $false 851 | 852 | if ($res.ContentLength -eq $app.size) { 853 | write-host 'UP TO DATE' -f green 854 | return 855 | } 856 | 857 | write-host 'OUTDATED' -f yellow 858 | 859 | if ($download) { 860 | $file = pint-download-app $app.id $app.arch $res 861 | pint-file-install $app.id $file $app.dir $app.arch 862 | } 863 | } catch { 864 | write-host $_ -f red 865 | } 866 | } 867 | } 868 | 869 | function pint-upgrade 870 | { 871 | $global:upgrade = $true 872 | pint-outdated @args 873 | } 874 | 875 | function pint-l 876 | { 877 | dir $env:PINT_APP_DIR -n -r -force -filter *.pint | split-path 878 | } 879 | 880 | function pint-list 881 | { 882 | $table = @() 883 | $fso = new-object -com Scripting.FileSystemObject 884 | 885 | pint-l |% { 886 | $app = pint-get-installed-app $_ 887 | 888 | $table += new-object -TypeName PSObject -Prop @{ 889 | ID = $app.id 890 | Directory = $_ + ' ' 891 | Size = '{0:N2} MB' -f (($fso.GetFolder($app.dir).Size) / 1MB) 892 | Version = (pint-get-version $app.dir) + $(if ($app.pinned) {' (pinned)'}) 893 | Arch = $app.arch 894 | } 895 | } 896 | 897 | $table | ft Directory,ID,Version,Size,Arch -autosize 898 | } 899 | 900 | function pint-self-update 901 | { 902 | write-host 'Fetching' $env:PINT_SELF_URL 903 | 904 | $res = get-text $env:PINT_SELF_URL 905 | 906 | if ($res -and $res.contains('PINT - Portable INsTaller')) { 907 | $res | out-file $env:PINT -encoding ascii 908 | write-host 'Pint was updated to the latest version.' -f green 909 | } else { 910 | write-host 'Self-update failed!' -f red 911 | exit 1 912 | } 913 | } 914 | 915 | function pint-forget 916 | { 917 | $args |% { 918 | try { 919 | get-pint $_ | del -force 920 | write-host $_ 'is no longer managed by Pint.' 921 | } catch { 922 | write-warning $_ 923 | } 924 | } 925 | } 926 | 927 | function pint-pin 928 | { 929 | $args |% { 930 | try { 931 | get-pint $_ | ren -NewName { $_.name -replace ' pinned','' -replace '.pint$',' pinned.pint' } 932 | write-host $_ 'is pinned.' 933 | } catch { 934 | write-warning $_ 935 | } 936 | } 937 | } 938 | 939 | function pint-unpin 940 | { 941 | $args |% { 942 | try { 943 | get-pint $_ | ren -NewName { $_.name -replace ' pinned','' } 944 | write-host $_ 'is unpinned.' 945 | } catch { 946 | write-warning $_ 947 | } 948 | } 949 | } 950 | 951 | function pint-search 952 | { 953 | $list = pint-app-list 954 | if ($args) { 955 | $term = '*' + ($args -join ' ' -replace '[^\w]+','*').trim('*') + '*' 956 | return $list |? { $_ -like $term } 957 | } 958 | $list 959 | } 960 | 961 | function pint-cleanup 962 | { 963 | dir (distdir '*') |% { 964 | write-host 'Removing' $_.Name 965 | del -r $_ 966 | } 967 | } 968 | 969 | function pint-shims([string]$dir, [string]$include, [string]$exclude, [bool]$delete) 970 | { 971 | $shimdir = $env:PINT_SHIM_DIR 972 | 973 | if (!$dir) { 974 | del (join-path $shimdir '*') -force 975 | $dir = $env:PINT_APP_DIR 976 | } 977 | 978 | $params = @{ 979 | recurse = $true 980 | force = $true 981 | exclude = clist $exclude 982 | ea = 0 983 | } 984 | 985 | if ($include) { 986 | $includeArr = clist $include 987 | $params.include = @('*.exe') + $includeArr 988 | } else { 989 | $params.filter = '*.exe' 990 | } 991 | 992 | ensure-dir $shimdir 993 | cd $shimdir 994 | 995 | dir $dir @params | Sort {$_.LastWriteTime} |% { 996 | if ($_.fullname.StartsWith($shimdir)) { return } 997 | 998 | $name = $_.name 999 | 1000 | if ($_.extension -eq '.exe' -and (!$includeArr -or !($includeArr |? { $name -like $_ }))) { 1001 | $subsystem = $null 1002 | try { 1003 | $fs = [IO.File]::OpenRead($_.fullname) 1004 | $br = new-object IO.BinaryReader $fs 1005 | if ($br.ReadUInt16() -ne 23117) { return } 1006 | $fs.Position = 0x3C 1007 | $fs.Position = $br.ReadUInt32() 1008 | $offset = $fs.Position 1009 | if ($br.ReadUInt32() -ne 17744) { return } 1010 | # $fs.Position += 0x14 1011 | # switch ($br.ReadUInt16()) { 0x10B { $arch = 32 } 0x20B { $arch = 64 } } 1012 | $fs.Position = $offset + 4 + 20 + 68 1013 | $subsystem = $br.ReadUInt16() 1014 | } catch {} finally { 1015 | if ($br) { $br.Close() } 1016 | if ($fs) { $fs.Close() } 1017 | } 1018 | if ($subsystem -ne 3) { return } 1019 | } 1020 | 1021 | $shim = join-path $shimdir $name 1022 | 1023 | if ($delete) { 1024 | if (is-file $shim) { 1025 | del $shim 1026 | write-host 'Removed' $name 1027 | } 1028 | } else { 1029 | $relpath = rvpa -relative -literalpath $_.fullname 1030 | & (get-dependency 'shimgen') -p $relpath -o $shim | out-null 1031 | write-host 'Added' $name 1032 | } 1033 | } 1034 | } 1035 | 1036 | function pint-info($app) 1037 | { 1038 | try { 1039 | pint-app-info $app 1040 | } catch { 1041 | write-host $_ -f red 1042 | } 1043 | } 1044 | 1045 | function pint-test([string]$subject, [string]$arch = $global:arch) 1046 | { 1047 | $env:PINT_CACHE_TTL = 0 1048 | 1049 | $list = if ($subject -match '[:\.]') { 1050 | $global:db = get-text $subject 1051 | ini-get-sections $global:db 1052 | } else { 1053 | pint-search $subject 1054 | } 1055 | 1056 | $pad = get-pad $list 1057 | 1058 | $list |% { 1059 | write-host $_.padright($pad, ' ') -nonewline 1060 | 1061 | try { 1062 | $res = make-app-request $_ $arch 1063 | write-host $res.ContentType ('(' + $res.ContentLength + ')') -f green 1064 | } catch { 1065 | write-host $_ -f red 1066 | } 1067 | } 1068 | } 1069 | 1070 | function pint-help 1071 | { 1072 | write-host "PINT - Portable INsTaller`n" -f white 1073 | write-host "Usage:" 1074 | write-host "pint ` ``n" -f yellow 1075 | write-host "Available commands:" 1076 | 1077 | @( 1078 | @('self-update', 'Update Pint.'), 1079 | @('search []', 'Search for an app in the database, or show all items.'), 1080 | @('installto [32|64] ', 'Install the app to the given directory.'), 1081 | @('install ', 'Install one or more apps to directories with the same names.'), 1082 | @('reinstall ', 'Force reinstallation of the package.'), 1083 | @('list', 'Show all applications installed via Pint.'), 1084 | @('l', 'Show only names of installed applications.'), 1085 | @('outdated []', 'Check for updates for all or some packages by your choice.'), 1086 | @('upgrade []', 'Install updates for all or selected apps.'), 1087 | @('pin ', 'Suppress updates for selected apps.'), 1088 | @('unpin ', 'Allow updates for selected apps (undoes the pin command).'), 1089 | @('remove ', 'Delete selected apps (this is equivalent to manual deletion).'), 1090 | @('purge ', 'Delete selected apps AND their archives.'), 1091 | @('cleanup', 'Delete archives from dist.'), 1092 | @('forget ', 'Stop tracking of selected apps.'), 1093 | @('download ', 'Only download selected installers without unpacking them.'), 1094 | @('shims', 'Recreate all shim files.'), 1095 | @('test [|] [32|64] ', 'Test app definitions.'), 1096 | @('info ', 'Show package configuration.'), 1097 | @('unpack ', 'Extract a file to a specified directory.') 1098 | ) |% { 1099 | write-host $_[0].padright(24, ' ') -f green -nonewline 1100 | write-host $_[1] 1101 | } 1102 | 1103 | write-host "`n` is a database ID, which can be seen via the search command." 1104 | write-host "` is a path, relative to the 'apps' directory, as shown via 'list' command." 1105 | 1106 | write-host "`nWorking directories:" -f yellow 1107 | 1108 | pint-dir-vars |% { 1109 | write-host "${_}:" ([Environment]::GetEnvironmentVariable($_)) 1110 | } 1111 | } 1112 | 1113 | function pint-dir-vars 1114 | { 1115 | @('PINT_APP_DIR', 'PINT_DIST_DIR', 'PINT_DEPS_DIR', 'PINT_SHIM_DIR') 1116 | } 1117 | 1118 | function pint-start($cmd) 1119 | { 1120 | pint-dir-vars |% { 1121 | $dir = [Environment]::GetEnvironmentVariable($_) 1122 | $dir = if (!(is-dir $dir)) { md $dir } else { gi $dir } 1123 | [Environment]::SetEnvironmentVariable($_, $dir.fullname) 1124 | } 1125 | 1126 | if (!$cmd) { pint-help; exit 0 } 1127 | 1128 | $cmd = 'pint-' + $cmd 1129 | 1130 | if (gcm $cmd -ea 0) { 1131 | & $cmd @args 1132 | exit $lastexitcode 1133 | } 1134 | 1135 | write-host 'Unknown command' 1136 | exit 1 1137 | } 1138 | --------------------------------------------------------------------------------