Search code examples
docker-composefish

How can I compose a list of option flags and pass them to a command in Fish Shell?


Since Docker Compose supports multiple Compose files, I want to detect all such files in the current directory and pass them to Docker Compose instead of needing to specify all of them manually. In Bash, I can accomplish this via:

docker compose $(for file in *.yml; do echo "-f $file "; done) up -d

I can produce the same list of flags in Fish with the following one-liner:

for file in *.yml; echo -n -- "-f $file "; end

I have tried various ways of passing that list to docker compose, including the following:

docker compose (for file in *.yml; echo -n -- "-f $file "; end) up -d

… but no matter how I try, I always get the following error:

stat /home/myuser/ compose-service1.yml -f compose-service2.yml -f compose-service3.yml -f compose-service4.yml -f compose-service5.yml: no such file or directory

My questions are:

  1. What must be done to ensure the list of option flags are passed to docker compose without problems?

  2. Is there another, saner approach, preferably even more concise than the (admittedly and obviously broken) approach I've taken above?


Solution

  • Fish splits command substitutions on newlines only.

    You are using echo -n, which explicitly doesn't add a newline. So your option printing code

    for file in *.yml; echo -n -- "-f $file "; end
    

    ends up printing something like

    -f compose-service1.yml -f compose-service2.yml -f compose-service3.yml -f compose-service4.yml -f compose-service5.yml

    As one line, which, to fish, is one argument. It ends up running like (truncated)

    docker compose "-f compose-service1.yml -f compose-service2.yml..." up -d
    

    So docker tries to find one file called

     compose-service1.yml -f compose-service2.yml -f compose-service3.yml -f compose-service4.yml -f compose-service5.yml
    

    (with the leading space!)

    Unlike bash, fish doesn't split this on spaces. This is generally a good idea, because people use filenames that include spaces. Your bash example is broken if a .yml file has spaces in the name.

    But because you want each -f and each filename to be a separate argument to docker compose, you need to ensure that the -f and the filename end up on separate lines.

    for file in *.yml; printf '%s\n' -f $file; end
    

    or

    for file in *.yml; string join \n -- -f $file; end
    

    is probably the simplest way to do that. This would print

    -f
    compose-service1.yml
    -f
    compose-service2.yml
    -f
    compose-service3.yml
    -f
    compose-service4.yml
    -f
    compose-service5.yml
    

    which fish would then turn into one argument per line.


    Alternatively:

    Docker's option parsing is reasonably standard, so you can also pass a compose file as -fcompose.yml, directly attached to the -f option. (this also explains your error - docker sees a -f and then uses the rest of the argument as the filename)

    That would allow using echo:

    for file in *.yml; echo -- "-f$file"; end
    

    This just prepends the -f and then prints it with a newline (no echo -n), which ends up printing

    -fcompose-service1.yml
    -fcompose-service2.yml
    

    (and so on)


    As an aside, if you really really really need a command substitution to be split on spaces, you can use string split " ". This is common with pkg-config and other *-config tools inspired by it.

    pkg-config --cflags --libs gio-2.0
    

    prints (truncated as an example)

    -pthread -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/glib-2.0

    which is meant to be used as-if copy-pasted into the commandline. (this means that pkg-config paths can't include spaces, btw)

    In that case you would use

    g++ example_01.cpp (pkg-config --cflags --libs gio-2.0 | string split -n " ")
    

    (the -n/--no-empty flag makes it ignore empty elements, which effectively means it treats runs of spaces as one thing).

    So, if you wanted to re-do your bash command like it is, with broken spaces in filenames, you could also use

    docker compose (for file in *.yml; echo -n -- "-f $file "; end | string split -n " ") up -d