2

Environment

  • Windows 11 23H2

  • Powershell v5.1

  • Docker desktop 4.49.0


Issue

I have a weird problem when I use variables as arguments for a docker run command inside a Powershell script (.ps1 file).

If I run the command without using variables in a Powershell session, it works fine:

docker run --rm -it -v "//c/host/path:/container/path" my_image

If I run the the same command as above inside a Powershell script, still without using variables, it works fine:

# foo.ps1
docker run --rm -it -v "//c/host/path:/container/path" my_image

The problem comes when I use a variable to store the volume mounting (-v ...):

# foo.ps1 
$volume = " -v `"//c/host/path:/container/path`" "
Write-Host docker run --rm -it ${volume} my_image
docker run --rm -it ${volume} my_image

Result of Write-Host (I replaced the host path for security reason, but the path is normal and has no special characters or whatsoever):

docker run --rm -it -v "//c/Users/rest_of_path:/home/ubuntu/ros2_ws" my_image:dev

Result of the docker run command:

docker: Error response from daemon: create  /c/Users/rest_of_path: " /c/Users/rest_of_path" includes invalid characters for a local volume name, only "[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed. If you intended to pass a host directory, use absolute path 

In the path I provide, the leading double slash is replaced by a leading blank and a single slash, and that must be the source of the problem. However, I don't know if it is related to Powershell/Windows or Docker, and I haven't found any solutions online.

New contributor
Damien is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
5
  • and if you just store the path under $volume, not the entire argument? because I think that's the issue.. there are a lot of spaces and quotes in that string currently. I would try: $volume = "//c/host/path:/container/path" and docker run --rm -it -v $volume my_image. Commented Nov 19 at 11:01
  • 1
    Use an array instead of a single string: $volume = "-v", "//c/host/path:/container/path". Issue is your variable is interpreted as a single argument instead of 2 Commented Nov 19 at 11:47
  • 1
    In short: In order to programmatically pass arguments to external programs, construct an array, with each parameter (option) name and parameter value / positional argument (operand) becoming its own element. E.g., to execute foo -o "bar baz", use $a = @('-o', 'bar baz'); foo $a. Note: You can not use a single string to encode multiple arguments. See the linked duplicate for details. Commented Nov 19 at 13:14
  • @ninadepina I need to store the entire argument as I sometimes want to not mount volume. Commented Nov 19 at 13:15
  • 1
    @SantiagoSquarzon thanks a lot that's the solution I need ! Commented Nov 19 at 13:16

2 Answers 2

2

The issue is that you're combining the -v with the volume name in your variable.
PowerShell will see the spaces in the variable, and automatically add double quotes around the expanded variable when calling the exe, making the command line effectively

docker run --rm -it " -v "//c/host/path:/container/path" " my_image

You can either:
Leave the -v as part of the command line and set the variable only to the path.

# foo.ps1 
$volume = "//c/host/path:/container/path"
Write-Host "docker run --rm -it -v `"$($volume)`" my_image"
docker run --rm -it -v "`"$($volume)`"" my_image

Or use splatting (while also keeping the -v and the volume as separate elements). Splatting will also allow you to create dynamic command lines.
Note that splatting can be combined with regular command line parameters. This example includes the image name as part of the splatted parameters, but it can also be moved back to the command line, and/or the --rm and -it could be moved to the splat array.

# foo.ps1 
$volume = "//c/host/path:/container/path"
$splat = @(
    '-v', "`"$($volume)`""
    'my_image'
)
Write-Host "docker run --rm -it $($splat -join ' ')"
docker run --rm -it @splat

In the command line, you should also wrap the variable in literal double quotes to force PowerShell 5 to add them around the path.

Sign up to request clarification or add additional context in comments.

5 Comments

The diagnosis of the problem is correct, but "`"$($volume)`"" is not only unnecessary - just $volume will do, with PowerShell providing behind-the-scenes double-quoting for values with spaces - it'll break in PowerShell 7.
This was specifically about 5.1, and adding the quotes helps in cases where there is no space in the string, but any other character that may cause issues as part of a command line. I've had issues before for that reason, and since then, for consistency's sake, I prefer to add them if in doubt.
The only scenario in which the ""`$var`"" workaround is needed is in the following edge case: a space-less argument that happens to contain cmd.exe metacharacters such as & that is passed to a batch file, owing to cmd.exe inappropriate parsing of process command lines as if they had been submitted from inside cmd.exe. Unless all three of these conditions are met - which is rare - your workaround is unnecessary, not to mention cumbersome and - to those familiar with standard quoting and escaping rules - confusing.
While you may personally choose to apply the workaround methodically (it isn't needed here), I suggest (a) explaining why you're doing it as part of your answer and (b) pointing out that it will break in PowerShell (Core) 7 for most CLIs, except those hard-wired to continue to exhibit the broken legacy behavior, with $PSNativeCommandArgumentPassing preference variable set to 'Legacy', which notably includes cmd.exe and batch files.
I understand that the question is about Windows PowerShell (the legacy, ships-with-Windows, Windows-only edition of PowerShell whose latest and final version is 5.1), but users (justifiably) assume that Window PowerShell solutions also work in PowerShell (Core) 7, the modern, cross-platform, install-on-demand edition, and they rarely pay attention to which PowerShell edition is being discussed (not least because that is often not specified). Therefore, it is always worth pointing out if a given solution only works in one of the two PowerShell editions.
0

Complementing user314159's helpful answer, it's always useful to have a version of print_argv.exe to visualize how arguments are being passed to your executable. Same comment in GitHub has the instructions to compile it but essentially, in PowerShell 5.1, paste the Add-Type code and run it, then you should have it in your current directory.

Then you can test how both versions look, for the first one we can see 5 arguments:

.\print_argv.exe run --rm -it -v '//c/host/path:/container/path' my_image
# "..\print_argv.exe" run --rm -it -v //c/host/path:/container/path my_image
# [0] run
# [1] --rm
# [2] -it
# [3] -v
# [4] //c/host/path:/container/path
# [5] my_image

Whereas for the second one, only 4, showing that -v and the path are passed as a single argument:

$volume = " -v `"//c/host/path:/container/path`" "
.\print_argv.exe run --rm -it ${volume} my_image
# "..\print_argv.exe" run --rm -it " -v \"//c/host/path:/container/path\" " my_image
# [0] run
# [1] --rm
# [2] -it
# [3]  -v "//c/host/path:/container/path"
# [4] my_image

As for alternatives that should work, including the ones shared by user314159, note that the additional layer of "..." for the path might not be needed if it doesn't contain any spaces (-v "`"$volume`"" might just be -v $volume):

# no need for `$(..)` here
$volume = "//c/host/path:/container/path"
.\print_argv.exe run --rm -it -v "`"$volume`"" my_image
# "..\print_argv.exe" run --rm -it -v "\"//c/host/path:/container/path\"" my_image
# [0] run
# [1] --rm
# [2] -it
# [3] -v
# [4] "//c/host/path:/container/path"
# [5] my_image

# again, no need for `$(..)`
$volume = "//c/host/path:/container/path"
$splat = @(
    '-v', "`"$volume`""
    'my_image'
)

.\print_argv.exe run --rm -it @splat
# "..\print_argv.exe" run --rm -it -v "\"//c/host/path:/container/path\"" my_image
# [0] run
# [1] --rm
# [2] -it
# [3] -v
# [4] "//c/host/path:/container/path"
# [5] my_image

And, an additional version that should also work is to have an array instead of a single string in your $volume:

$volume = "-v", "//c/host/path:/container/path"
.\print_argv.exe run --rm -it ${volume} my_image
# "..\print_argv.exe" run --rm -it " -v \"//c/host/path:/container/path\" " my_image
# [0] run
# [1] --rm
# [2] -it
# [3] -v
# [4] //c/host/path:/container/path
# [5] my_image

2 Comments

Note that "`"$($volume)`"" is not only unnecessary - just $volume will do, with PowerShell providing behind-the-scenes double-quoting for values with spaces - it'll break in PowerShell 7.
There is only one edge case, detailed in my comments on user314159's answer, where "`"$($volume)`"" is necessary, but it doesn't apply here (and rarely does). Saying "might not be needed if it doesn't contain any spaces" is misleading.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.