Repository: pre-commit/pre-commit

Issue:
Run hooks on files in specific dir, rather than using `--all-files`
I am able to successfully use `pre-commit run --all-files` to run hooks on all my files.

Now, I'm trying to run the hooks just on files in a particular directory. I think I'm not understanding the docs and I can't find an example to work from.

Here's what the docs say:
> `--files [FILES [FILES ...]]`: specific filenames to run hooks on.

I've tried the following variations:
`pre-commit run --files web/modules/custom`
`pre-commit run --files web/modules/custom/*`
`pre-commit run --files [web/modules/custom]`
`pre-commit run --files [web/modules/custom/*]`
`pre-commit run --files [FILES [web/modules/custom]`
`pre-commit run --files [FILES [web/modules/custom/*]`

I feel really dumb having to ask, but can someone please point me in the right direction?


Discussion:
the second one should work on posix (with glob support)
oh right but you're on Windows right?
Correct
do you have access to coreutils (in particular xargs):

```
git ls-files -- web/modules/custom | xargs pre-commit run --files
```
I do have xargs, but when I run the command you provided, I get the following output:

```
λ git ls-files -- web\modules\custom\ | xargs pre-commit run --files
Trim Trailing Whitespace.................................................Passed
PHP Codesniffer......................................(no files to check)Skipped
```
hmmm that looks good, can you show the following:

```
cat .pre-commit-config.yaml
git ls-files -- web\modules\custom\
```

```
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v2.0.0
  hooks:
  - id: trailing-whitespace

- repo: https://github.com/steigenTI/pre-commit-php.git
  rev: 1.3.1
  hooks:
  - id: php-cs
    files: ^web\/(modules\/custom|themes\/my_theme)\/.+.php$
    args: [--standard=Drupal -p]
```

Running `git ls-files -- web\modules\custom\` correctly outputs all the files in that directory (I'd rather not paste the list here). Many `.php` files are listed. When running `pre-commit run --all-files`, MANY of these files have errors that are detected by the `php-cs` hook.

The result in my last comment seems wrong to me -- I would expect PHP Codesniffer to have checked a bunch of files, rather than saying "no files to check".
I suspect that `files` regex is not matching the filenames you expect

```pycon
>>> import yaml
>>> yaml.load('files: ^web\/(modules\/custom|themes\/my_theme)\/.+.php$')
{'files': '^web\\/(modules\\/custom|themes\\/my_theme)\\/.+.php$'}
>>> import re
>>> reg = re.compile('^web\\/(modules\\/custom|themes\\/my_theme)\\/.+.php$')
>>> reg.match('web/modules/custom/foo.php')
<_sre.SRE_Match object; span=(0, 26), match='web/modules/custom/foo.php'>
>>> reg.match(r'web\modules\custom\foo.php')
>>> 
```
I tested my regex using https://regexr.com/ and it seemed ok.

Also, when I run `pre-commit run --all-files` the files listed correspond to the regex.

I'm a little confused, because I thought that `pre-commit run --files web/modules/custom/*` would override the `-files` property in `.pre-commit-config.yml`.
My suspicion is a mismatch of forward and backward slashes such that the regex isn't matching -- you can debug what filenames are getting matched by using the `identity` meta hook, for instance:

```yaml
-   repo: meta
    -   id: identity
        name: identity (filtered)
        files: ^web\/(modules\/custom|themes\/my_theme)\/.+.php$
    -   id: identity
        name: identity (all)
```

and then you can

`pre-commit run identity --all-files`

or

`git ls-files -- web\modules\custom\ | xargs pre-commit run identity --files`
Running `pre-commit run identity --all-files` gives me the list of files that I expect.

The other command, unfortunately...
```
λ git ls-files -- web\modules\custom\ | xargs pre-commit run identity --files
identity (filtered)..................................(no files to check)Skipped
```
Ps. you have a small typo in your code above

> ```yaml
> -   repo: meta
>     hooks:
>     -   id: identity
>         name: identity (filtered)
>         files: ^web\/(modules\/custom|themes\/my_theme)\/.+.php$
>     -   id: identity
>         name: identity (all)
> ```



does `identity (all)` with the `xargs` command list them? do they have forward slashes or backslashes?
Forward slashes.
can you share one filename from that output? feel free to obfuscate the parts after `web/modules/custom/...`
`web/modules/custom/ckeditor_textarearesize/src/Plugin/CKEditorPlugin/Resize.php`
that's positively puzzling! I'll have to dig into this some more and see if I can't craft a repository which reproduces this :thinking:
ok I'm seeing backslashes... but only when piped?

```
(venv) C:\Users\IEUser\AppData\Local\Temp\x>git ls-files -- web\common | xargs pre-commit run --files
identity (filtered)..................................(no files to check)Skipped
identity (all)...........................................................Passed
hookid: identity

web\common\hello.php


(venv) C:\Users\IEUser\AppData\Local\Temp\x>git ls-files -- web\common
web/common/hello.php
```

can you try and adjusted regex where you replace `\/` with `[\/]`?
Okay. I modified the regex. My config file now looks like this:

```
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v2.3.0
  hooks:
  - id: trailing-whitespace

- repo: meta
  hooks:
    - id: identity
      name: identity (filtered)
      files: ^web[\/](modules[\/]custom|themes[\/]my_theme)[\/].+.php$
    - id: identity
      name: identity (all)

- repo: https://github.com/steigenTI/pre-commit-php.git
  rev: 1.3.1
  hooks:
  - id: php-cs
    files: ^web[\/](modules[\/]custom|themes[\/]my_theme)[\/].+.php$
    args: [--standard=Drupal -p]
```

After this change, `pre-commit run --all-files` still works as expected.

But when attempting to run on a set of files, I still get no results (there should be files to check):
```
λ pre-commit run --files web/modules/custom/*
Trim Trailing Whitespace.............................(no files to check)Skipped
identity (filtered)..................................(no files to check)Skipped
identity (all).......................................(no files to check)Skipped
PHP Codesniffer......................................(no files to check)Skipped
```

the `*` glob will only work on linuxlike platforms (or when invoked using `git bash` / `cygwin` for instance)
 -- does the `xargs` approach from above work?
I included some file paths that I don't mind sharing...

```
λ git ls-files -- web/modules/custom | xargs pre-commit run --files
Trim Trailing Whitespace.................................................Passed
identity (filtered)..................................(no files to check)Skipped
identity (all)...........................................................Passed
hookid: identity
[...]
web\modules\custom\custom_twig\readme.txt
[...]
web\modules\custom\ckeditor_textarearesize\ckeditor_textarearesize.module
[...]
web\modules\custom\ckeditor_textarearesize\ckeditor_textarearesize.info.yml
[...]
web\modules\custom\custom_twig\custom_twig.services.yml
[...]
web\modules\custom\.editorconfig
[...]
web\modules\custom\custom_twig\custom_twig.info.yml
[...]

PHP Codesniffer......................................(no files to check)Skipped
```
urgh escape sequences right -- try `[\\/]` instead of `[\/]` :man_facepalming: 
(what all this tells me is that pre-commit should probably maybe do some slash normalization to make this less pain)
Same result as above.
hmmmmm! this is working for me :S

```
(venv) C:\Users\IEUser\AppData\Local\Temp\x>git ls-files -- web\ | xargs pre-commit run --files
identity (filtered)......................................................Passed
hookid: identity

web\common\hello.php

identity (all)...........................................................Passed
hookid: identity

web\common\hello.php


(venv) C:\Users\IEUser\AppData\Local\Temp\x>cat .pre-commit-config.yaml
repos:
-   repo: meta
    hooks:
    -   id: identity
        name: identity (filtered)
        files: ^web[\\/]common[\\/].+.php$
    -   id: identity
        name: identity (all)
```
I modified my config file again to remove the "files" attribute entirely.

```
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v2.3.0
  hooks:
  - id: trailing-whitespace

- repo: meta
  hooks:
    - id: identity
      name: identity (filtered)
    - id: identity
      name: identity (all)

- repo: https://github.com/steigenTI/pre-commit-php.git
  rev: 1.3.1
  hooks:
  - id: php-cs
    args: [--standard=Drupal -p]
```

Now, when I execute `git ls-files -- web/modules/custom | xargs pre-commit run --files` it seems like the slashes are being stripped out on their way to phpcs.

```
PHP Codesniffer..........................................................Failed
hookid: php-cs

Begin PHP Codesniffer ...
Running command phpcs --standard=Drupal -p web\modules\custom\custom_twig\src\customTwigExtension.php [...] web\modules\custom\ckeditor_textarearesize\src\Plugin\CKEditorPlugin\Resize.php [...]
Errors detected by PHP CodeSniffer ...
ERROR: The file "webmodulescustomcustom_twigsrccustomTwigExtension.php" does not exist.
[...]
```

When I run the command `phpcs --standard=Drupal -p web\modules\custom\custom_twig\src\customTwigExtension.php` manually, I get proper phpcs output.
that's probably a bug with the shell script that implements that hook -- this style of command building is the problem: https://github.com/steigenTI/pre-commit-php/blob/c9d21333c916b7074ef45d864567cee1bfbef363/pre_commit_hooks/php-cs.sh#L24

here's more on that subject: https://github.com/asottile/scratch/wiki/Bash:-conditional-arguments

(note also that building command strings and evaling in that way is a common source of CVEs)

Resolving patch:
diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py
--- a/pre_commit/commands/run.py
+++ b/pre_commit/commands/run.py
@@ -34,6 +34,12 @@ def filter_by_include_exclude(names, include, exclude):
 
 class Classifier(object):
     def __init__(self, filenames):
+        # on windows we normalize all filenames to use forward slashes
+        # this makes it easier to filter using the `files:` regex
+        # this also makes improperly quoted shell-based hooks work better
+        # see #1173
+        if os.altsep == '/' and os.sep == '\\':
+            filenames = (f.replace(os.sep, os.altsep) for f in filenames)
         self.filenames = [f for f in filenames if os.path.lexists(f)]
         self._types_cache = {}
 


Test patch:
diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py
--- a/tests/commands/run_test.py
+++ b/tests/commands/run_test.py
@@ -7,6 +7,7 @@
 import subprocess
 import sys
 
+import mock
 import pytest
 
 import pre_commit.constants as C
@@ -782,7 +783,7 @@ def test_files_running_subdir(repo_with_passing_hook, tempdir_factory):
                 '--files', 'foo.py',
                 tempdir_factory=tempdir_factory,
             )
-        assert 'subdir/foo.py'.replace('/', os.sep) in stdout
+        assert 'subdir/foo.py' in stdout
 
 
 @pytest.mark.parametrize(
@@ -826,6 +827,23 @@ def test_classifier_removes_dne():
     assert classifier.filenames == []
 
 
+def test_classifier_normalizes_filenames_on_windows_to_forward_slashes(tmpdir):
+    with tmpdir.as_cwd():
+        tmpdir.join('a/b/c').ensure()
+        with mock.patch.object(os, 'altsep', '/'):
+            with mock.patch.object(os, 'sep', '\\'):
+                classifier = Classifier((r'a\b\c',))
+                assert classifier.filenames == ['a/b/c']
+
+
+def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir):
+    with mock.patch.object(os.path, 'lexists', return_value=True):
+        with mock.patch.object(os, 'altsep', None):
+            with mock.patch.object(os, 'sep', '/'):
+                classifier = Classifier((r'a/b\c',))
+                assert classifier.filenames == [r'a/b\c']
+
+
 @pytest.fixture
 def some_filenames():
     return (