Skip to content

V2 #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 18 commits into from
Closed

V2 #8

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
language: node_js

node_js:
- 11
- lts/carbon
- lts/dubnium

env:
- YARN_GPG=no # https://travis-ci.community/t/timeout-after-build-finished-and-succeeded/1336

os:
- linux
- windows

notifications:
email: false
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Change Log

# 2.0.0 (2019-04-26)

Rewrite, transfer to Profiscience GitHub org, set up CI

### BREAKING CHANGES

- Use named exports instead of exporting sync func by default (was 'module.exports = trueCasePathSync`, now `module.exports = { trueCasePath, trueCasePathSync }`)
- If relying on (undocumented) glob options, those will no longer work
- Drop support for Node <=6

### Features

- Async version of function
- TypeScript definitions

### Bug Fixes

- **Windows:** [Drive letters](https://github.com/barsh/true-case-path/issues/3)
- **Windows:** [Special characters in file path](https://github.com/barsh/true-case-path/issues/5)
46 changes: 13 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,52 +1,32 @@
# true-case-path

## Usage
> Given a possibly case-variant version of an existing filesystem path, returns the absolute, case-exact, normalized version as stored in the filesystem.

`trueCasePathSync(<fileSystemPath>)`
## Usage

## Description
```typescript
const { trueCasePath, trueCasePathSync } = require('true-case-path')

Given a possibly case-variant version of an existing filesystem path, returns
the case-exact, normalized version as stored in the filesystem.
trueCasePath(<fileSystemPath>)
.then((caseCorrectPath) => {
// ...
})

If the input path is a globbing *pattern* as defined by the 'glob' npm
package, only the 1st match, if any, is returned.
Only a literal input path guarantees an unambiguous result.
const caseCorrectPath = trueCasePathSync(<fileSystemPath>)
```

If no matching path exists, undefined is returned.
On case-SENSITIVE filesystems, a match will also be found, but if case
variations of a given path exist, it is undefined which match is returned.
> **NOTE**: If no matching path exists, an error with be thrown.

## Platforms

Windows, OSX, and Linux (though note the limitations with case-insensitive filesystems).

## Limitations

- Paths starting with `'./'` are acceptable, but paths starting with `'../'`
are not - when in doubt, resolve with `fs.realPathSync()` first.
An initial `'.'` and *interior* `'..'` instances are normalized, but a relative
input path still results in a relative output path. If you want to ensure
an absolute output path, apply `fs.realPathSync()` to the result.
- On Windows, no attempt is made to case-correct the drive letter or UNC-share
component of the path.
- Unicode support:
- Be sure to use UTF8 source-code files (with a BOM on Windows)
- On OSX, the input path is automatically converted to NFD Unicode form
to match how the filesystem stores names, but note that the result will
invariably be NFD too (which makes no difference for ASCII-characters-only
names).
Windows, OSX, and Linux

## Examples

```
const trueCasePathSync = require('true-case-path')
const { trueCasePathSync } = require('true-case-path')

trueCasePathSync('/users/guest') // OSX: -> '/Users/Guest'

trueCasePathSync('c:\\users\\all users') // Windows: -> 'c:\Users\All Users'
```

## Attribution

The code for this project was sourced from [http://stackoverflow.com/a/33139702/45375](http://stackoverflow.com/a/33139702/45375)
3 changes: 3 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function trueCasePath(filePath: string): Promise<string>

export function trueCasePathSync(filePath: string): string
85 changes: 57 additions & 28 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,61 @@
'use strict'

var glob = require('glob')
var path = require('path')

function trueCasePathSync(fsPath) {

// Normalize the path so as to resolve . and .. components.
// !! As of Node v4.1.1, a path starting with ../ is NOT resolved relative
// !! to the current dir, and glob.sync() below then fails.
// !! When in doubt, resolve with fs.realPathSync() *beforehand*.
var fsPathNormalized = path.normalize(fsPath)

// OSX: HFS+ stores filenames in NFD (decomposed normal form) Unicode format,
// so we must ensure that the input path is in that format first.
if (process.platform === 'darwin') fsPathNormalized = fsPathNormalized.normalize('NFD')

// !! Windows: Curiously, the drive component mustn't be part of a glob,
// !! otherwise glob.sync() will invariably match nothing.
// !! Thus, we remove the drive component and instead pass it in as the 'cwd'
// !! (working dir.) property below.
var pathRoot = path.parse(fsPathNormalized).root
var noDrivePath = fsPathNormalized.slice(Math.max(pathRoot.length - 1, 0))

// Perform case-insensitive globbing (on Windows, relative to the drive /
// network share) and return the 1st match, if any.
// Fortunately, glob() with nocase case-corrects the input even if it is
// a *literal* path.
return glob.sync(noDrivePath, { nocase: true, cwd: pathRoot })[0]
const fs = require('fs')
const os = require('os')
const path = require('path')
const { promisify: pify } = require('util')

const readdir = pify(fs.readdir)
const isWindows = os.platform() === 'win32'
const delimiter = isWindows ? '\\' : '/'

module.exports = {
trueCasePath,
trueCasePathSync
}

function getFilePathSegments(filePath) {
return path.resolve(process.cwd(), filePath).split(delimiter).filter((s) => s !== '')
}

function escapeString(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

function matchCaseInsensitive(fileOrDirectory, directoryContents, filePath) {
const caseInsensitiveRegex = new RegExp(`^${escapeString(fileOrDirectory)}$`, 'i')
for (const file of directoryContents) {
if (caseInsensitiveRegex.test(file)) {
return file
}
}
throw new Error(`[true-case-path]: Called with ${filePath}, but no matching file exists`)
}

async function trueCasePath(filePath) {
const segments = getFilePathSegments(filePath)
let realPath = ''
if (isWindows) {
realPath += segments.shift().toUpperCase() // drive letter
}
for (const fileOrDirectory of segments) {
const contents = await readdir(realPath + delimiter)
const realCaseFileOrDirectory = matchCaseInsensitive(fileOrDirectory, contents, filePath)
realPath += delimiter + realCaseFileOrDirectory
}
return realPath
}

module.exports = trueCasePathSync
function trueCasePathSync(filePath) {
const segments = getFilePathSegments(filePath)
let realPath = ''
if (isWindows) {
realPath += segments.shift().toUpperCase() // drive letter
}
for (const fileOrDirectory of segments) {
const contents = fs.readdirSync(realPath + delimiter)
const realCaseFileOrDirectory = matchCaseInsensitive(fileOrDirectory, contents, filePath)
realPath += delimiter + realCaseFileOrDirectory
}
return realPath
}
17 changes: 5 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,19 @@
"description": "Given a possibly case-variant version of an existing filesystem path, returns the case-exact, normalized version as stored in the filesystem.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/barsh/true-case-path.git"
"url": "git+https://github.com/Profiscience/true-case-path.git"
},
"author": "barsh",
"contributors": [
{
"name": "Michael Klement",
"email": "mklement0@gmail.com",
"url": "https://github.com/mklement0"
}
"Casey Webb <notcaseywebb@gmail.com> (https://caseyWebb.xyz)"
],
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/barsh/true-case-path/issues"
"url": "https://github.com/Profiscience/true-case-path/issues"
},
"homepage": "https://github.com/barsh/true-case-path#readme",
"dependencies": {
"glob": "^7.1.2"
}
"homepage": "https://github.com/Profiscience/true-case-path#readme"
}
Empty file added test/fixture/fOoBaR/BAZ
Empty file.
Empty file added test/fixture/f[u&n%k)y
Empty file.
43 changes: 43 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict'

const assert = require('assert')
const path = require('path')

const {
trueCasePath,
trueCasePathSync
} = require('../')

const expected = path.join(__dirname, 'fixture/fOoBaR/BAZ')
const requested = expected.toLowerCase()

function testSync() {
assert.equal(trueCasePathSync(requested), expected, 'trueCasePathSync works')
}

function testAsync() {
return trueCasePath(requested).then((actual) => assert.equal(actual, expected, 'trueCasePath (async) works'))
}

function testRelative() {
assert.equal(trueCasePathSync('test/fixture/fOoBaR/BAZ'), expected, 'works with relative paths')
}

function testSpecialChars() {
assert.equal(trueCasePathSync('test/fixture/F[U&N%K)Y'), path.join(__dirname, 'fixture/f[u&n%k)y'), 'works with file names w/ special chars')
}

Promise.all([
testSync(),
testRelative(),
testAsync(),
testSpecialChars()
])
.then(() => {
console.log('All tests passed!')
})
.catch((err) => {
console.log('Test failed!')
console.error(err)
process.exitCode = 1
})
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1