Repository: ajalt/clikt (kotlin)

Issue:
Autocomplete fails to complete filenames with spaces
When I try the autocomplete feature in bash with file names that contain spaces, the completion fails, because the parts of the name are treated separately. 

So if I have a file named `file with spaces.txt` and type `fi`, then nothing happens on first TAB (despite no other file matches `fi`), and on the second TAB I get the list: `file        spaces.txt  with` as possible completions, which is the correctly found filename, but the parts sorted alphabetically. 

It looks for me like the result of some missing quotes or something like that, but I know very few of bash autocompletion, so I don't know how to fix it.

Pull request description:
Fixes #379

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39e9e030..36deb159 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 ### Fixed
 - Fixed `@argfiles` not being expanded when specified after a subcommand ([#570](https://github.com/ajalt/clikt/pull/570))
 - Fixed syntax error in generated bash completions when an argument name contained spaces ([#563](https://github.com/ajalt/clikt/pull/563))
+- Support bash completions of file parameters when file names contain spaces or special characters ([#379](https://github.com/ajalt/clikt/pull/379))
 
 ## 5.0.2
 ### Changed
diff --git a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/completion/BashCompletionGenerator.kt b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/completion/BashCompletionGenerator.kt
index 3a0adb5f..07fb7f93 100644
--- a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/completion/BashCompletionGenerator.kt
+++ b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/completion/BashCompletionGenerator.kt
@@ -18,7 +18,7 @@ internal object BashCompletionGenerator {
             .flatMap { arg -> (1..arg.nvalues).map { "'${arg.name}'" } }
             .joinToString(" ")
         val varargName = command._arguments.find { it.nvalues < 0 }?.name.orEmpty()
-        val paramsWithCandidates =
+        val paramsWithCandidates: List<Pair<String, CompletionCandidates>> =
             (options.map { o -> o.first.maxByOrNull { it.length }!! to o.second } + arguments)
 
         if (options.isEmpty() && subcommands.isEmpty() && arguments.isEmpty()) return ""
@@ -50,15 +50,26 @@ internal object BashCompletionGenerator {
 
                 append(
                     """
-                |__skip_opt_eq() {
-                |    # this takes advantage of the fact that bash functions can write to local
-                |    # variables in their callers
-                |    (( i = i + 1 ))
-                |    if [[ "${'$'}{COMP_WORDS[${'$'}i]}" == '=' ]]; then
-                |          (( i = i + 1 ))
-                |    fi
-                |}
-                |
+        |__skip_opt_eq() {
+        |    # this takes advantage of the fact that bash functions can write to local
+        |    # variables in their callers
+        |    (( i = i + 1 ))
+        |    if [[ "${'$'}{COMP_WORDS[${'$'}i]}" == '=' ]]; then
+        |          (( i = i + 1 ))
+        |    fi
+        |}
+        |
+        |__complete_files() {
+        |   # Generate filename completions
+        |   local word="${'$'}1"
+        |   local IFS=${'$'}'\n'
+        |
+        |   # quote each completion to support spaces and special characters
+        |   COMPREPLY=(${'$'}(compgen -o filenames -f -- "${'$'}word" | while read -r line; do
+        |       printf "%q\n" "${'$'}line"
+        |   done))
+        |}
+        |
                 """.trimMargin()
                 )
             }
@@ -233,7 +244,7 @@ internal object BashCompletionGenerator {
                     }
 
                     CompletionCandidates.Path -> {
-                        append("       COMPREPLY=(\$(compgen -o default -- \"\${word}\"))\n")
+                        append("       __complete_files \"\${word}\"\n")
                     }
 
                     CompletionCandidates.Hostname -> {


Test patch:
diff --git a/test/src/commonTest/kotlin/com/github/ajalt/clikt/completion/BashCompletionTest.kt b/test/src/commonTest/kotlin/com/github/ajalt/clikt/completion/BashCompletionTest.kt
index 354f67bd..c04d931d 100644
--- a/test/src/commonTest/kotlin/com/github/ajalt/clikt/completion/BashCompletionTest.kt
+++ b/test/src/commonTest/kotlin/com/github/ajalt/clikt/completion/BashCompletionTest.kt
@@ -17,6 +17,17 @@ class BashCompletionTest : CompletionTestBase("bash") {
         |    fi
         |}
         |
+        |__complete_files() {
+        |   # Generate filename completions
+        |   local word="${'$'}1"
+        |   local IFS=${'$'}'\n'
+        |
+        |   # quote each completion to support spaces and special characters
+        |   COMPREPLY=(${'$'}(compgen -o filenames -f -- "${'$'}word" | while read -r line; do
+        |       printf "%q\n" "${'$'}line"
+        |   done))
+        |}
+        |
         |__c_complete___o() {
         |  COMPREPLY=(${'$'}(compgen -W "${'$'}(echo foo bar)" -- "${'$'}{COMP_WORDS[${'$'}COMP_CWORD]}"))
         |}
@@ -100,11 +111,22 @@ class BashCompletionTest : CompletionTestBase("bash") {
         |    # this takes advantage of the fact that bash functions can write to local
         |    # variables in their callers
         |    (( i = i + 1 ))
-        |    if [[ "${"$"}{COMP_WORDS[${"$"}i]}" == '=' ]]; then
+        |    if [[ "${'$'}{COMP_WORDS[${'$'}i]}" == '=' ]]; then
         |          (( i = i + 1 ))
         |    fi
         |}
         |
+        |__complete_files() {
+        |   # Generate filename completions
+        |   local word="${'$'}1"
+        |   local IFS=${'$'}'\n'
+        |
+        |   # quote each completion to support spaces and special characters
+        |   COMPREPLY=(${'$'}(compgen -o filenames -f -- "${'$'}word" | while read -r line; do
+        |       printf "%q\n" "${'$'}line"
+        |   done))
+        |}
+        |
         |_c() {
         |  local i=1
         |  local in_param=''
@@ -381,6 +403,17 @@ class BashCompletionTest : CompletionTestBase("bash") {
         |    fi
         |}
         |
+        |__complete_files() {
+        |   # Generate filename completions
+        |   local word="${'$'}1"
+        |   local IFS=${'$'}'\n'
+        |
+        |   # quote each completion to support spaces and special characters
+        |   COMPREPLY=(${'$'}(compgen -o filenames -f -- "${'$'}word" | while read -r line; do
+        |       printf "%q\n" "${'$'}line"
+        |   done))
+        |}
+        |
         |_c() {
         |  local i=1
         |  local in_param=''
@@ -455,6 +488,17 @@ class BashCompletionTest : CompletionTestBase("bash") {
         |    fi
         |}
         |
+        |__complete_files() {
+        |   # Generate filename completions
+        |   local word="${'$'}1"
+        |   local IFS=${'$'}'\n'
+        |
+        |   # quote each completion to support spaces and special characters
+        |   COMPREPLY=(${'$'}(compgen -o filenames -f -- "${'$'}word" | while read -r line; do
+        |       printf "%q\n" "${'$'}line"
+        |   done))
+        |}
+        |
         |_c() {
         |  local i=1
         |  local in_param=''
@@ -525,7 +569,7 @@ class BashCompletionTest : CompletionTestBase("bash") {
         |    "--none")
         |      ;;
         |    "--path")
-        |       COMPREPLY=(${'$'}(compgen -o default -- "${'$'}{word}"))
+        |       __complete_files "${'$'}{word}"
         |      ;;
         |    "--host")
         |       COMPREPLY=(${'$'}(compgen -A hostname -- "${'$'}{word}"))
@@ -564,6 +608,17 @@ class BashCompletionTest : CompletionTestBase("bash") {
         |    fi
         |}
         |
+        |__complete_files() {
+        |   # Generate filename completions
+        |   local word="${'$'}1"
+        |   local IFS=${'$'}'\n'
+        |
+        |   # quote each completion to support spaces and special characters
+        |   COMPREPLY=(${'$'}(compgen -o filenames -f -- "${'$'}word" | while read -r line; do
+        |       printf "%q\n" "${'$'}line"
+        |   done))
+        |}
+        |
         |_c() {
         |  local i=1
         |  local in_param=''