How to escape bash for in array variable

Hello i am at the moment stuck on how to escape an array variable "${STATIC_ENV_VARS[@]}" in the following code:

  installPhase = ''
...
mapfile -t STATIC_ENV_VARS < <(${yq-go}/bin/yq '.env[] | select(test("^\\[macos\\]") == false) | sub("^\\[linux\\]\\s*", "")' "\$Static_LAUNCHER_YML" 2>/dev/null)
for env_item in "${STATIC_ENV_VARS[@]}"; do
    if [ -n "\$env_item" ]; then
        export "\$env_item"
    fi
done
...
  '';

What i tried:
"''\${STATIC_ENV_VARS[@]}"""
"''\$''\{STATIC_ENV_VARS[@]''\}"""
"\${STATIC_ENV_VARS[@]}" → error
"\$\{STATIC_ENV_VARS[@]}""$\{DYNAMIC_ENV_VARS[@]}"
"$''\{STATIC_ENV_VARS[@]}"""
"$\{STATIC_ENV_VARS[@]}""$\{DYNAMIC_ENV_VARS[@]}"

I don’t know what i do wrong…

In intended strings you escape the ${ by using ''${.

   installPhase = ''
 ...
 mapfile -t STATIC_ENV_VARS < <(${yq-go}/bin/yq '.env[] | select(test("^\\[macos\\]") == false) | sub("^\\[linux\\]\\s*", "")' "\$Static_LAUNCHER_YML" 2>/dev/null)
-for env_item in "${STATIC_ENV_VARS[@]}"; do
+for env_item in "''${STATIC_ENV_VARS[@]}"; do
     if [ -n "\$env_item" ]; then
         export "\$env_item"
     fi
 done
 ...
   '';

thanks for the fast reply.
It didn’t work for me:

for env_item in "''${STATIC_ENV_VARS[@]}"; do
    if [ -n "\$env_item" ]; then
        export "\$env_item"
    fi
done

"''${STATIC_ENV_VARS[@]}"""

What does this mean? Do you see "" in the rendered script? Or is the STATIC_ENV_VARS just empty?

As a matter of debugging, you might want to add a simple echo $STATIC_ENV_VARS between mapfile and the loop.

I see "" in the rendered script.

Can you please share a full reproducer?

Sure.
I try to build it with nix-build and it doesn’t work.

The code is in package.nix line 125 or 131.


default.nix
let
  pkgs = import <nixpkgs> {};
in
pkgs.callPackage ./package.nix {}
package.nix
{ stdenv
, fetchurl
, makeDesktopItem
, copyDesktopItems
, autoPatchelfHook
, unzip
, zlib
, glibc
, fontconfig
, alsa-lib
, xorg
, wayland
, gcc13
, libzen
, libmediainfo
, zenity
, lib
, ffmpeg
, yt-dlp
, deno
, openjdk21
, yq-go
}:

let
  version = "5.2.12";

  # Define sources for different architectures
  sources = {
    "x86_64-linux" = {
      arch = "amd64";
      sha256 = "sha256-0v5zSUmY2JuCiE0VPaL0iayOm0RIlcbc3CYP9vaUxPM=";
    };
    "aarch64-linux" = {
      arch = "arm64";
      sha256 = "sha256-rXT3HKczUtvoebjmCxm0S/l1dIRkolk40qdL/FVvrK8=";
    };
  };

  # Select the source based on the current system
  sysSrc = sources.${stdenv.hostPlatform.system}
    or (throw "Unsupported system: ${stdenv.hostPlatform.system}");

  jdk = openjdk21;
  desktopItem = makeDesktopItem {
    name = "tinyMediaManager";
    exec = "tinyMediaManager";
    icon = "tinyMediaManager";
    comment = "A media management tool";
    desktopName = "tinyMediaManager";
    genericName = "Media Manager";
    categories = [ "Video" "AudioVideo" ];
    terminal = false;
  };

in
stdenv.mkDerivation {
  pname = "tinyMediaManager";
  inherit version;

  src = fetchurl {
    url = "https://archive.tinymediamanager.org/v${version}/tinyMediaManager-${version}-linux-${sysSrc.arch}.tar.xz";
    sha256 = sysSrc.sha256;
  };

  nativeBuildInputs = [
    autoPatchelfHook
    unzip
    copyDesktopItems
  ];

  buildInputs = [
    zlib
    glibc
    fontconfig
    alsa-lib
    xorg.libX11
    xorg.libXext
    xorg.libXrender
    xorg.libXtst
    xorg.libXi
    wayland
    gcc13.cc.lib
    libzen
    libmediainfo
    zenity
  ];

  strictDeps = true;
  __structuredAttrs = true;

  desktopItems = [ desktopItem ];

  installPhase = ''
    runHook preInstall

    # Create destination directory
    mkdir -p $out/opt/tinyMediaManager $out/bin

    cp -r * $out/opt/tinyMediaManager


    cat > $out/bin/tinyMediaManager << EOF
#!/usr/bin/env bash
export LD_LIBRARY_PATH=${libzen}/lib:${libmediainfo}/lib:\$LD_LIBRARY_PATH
export PATH=${zenity}/bin:${yt-dlp}/bin:${ffmpeg}/bin:${deno}/bin:\$PATH

Static_LAUNCHER_YML="$out/opt/tinyMediaManager/launcher.yml"
# 1. Parse JVM Options
Static_OPTS=\$(${yq-go}/bin/yq '.jvmOpts[] | select(test("^\\[macos\\]") == false) | sub("^\\[linux\\]\\s*", "")' "\$Static_LAUNCHER_YML" | xargs)
# 2. Parse Classpath
Static_CLASSPATH=\$(${yq-go}/bin/yq '.classpath[] | select(test("^\\[macos\\]") == false) | sub("^\\[linux\\]\\s*", "") | sub("^CONTENT_DIR", "~/.local/share/tinyMediaManager") | sub("^/", "")' "\$Static_LAUNCHER_YML" | paste -sd ":" -)
# 3. Parse env
mapfile -t STATIC_ENV_VARS < <(${yq-go}/bin/yq '.env[] | select(test("^\\[macos\\]") == false) | sub("^\\[linux\\]\\s*", "")' "\$Static_LAUNCHER_YML" 2>/dev/null)

LAUNCHER_YML="\$HOME/.local/share/tinyMediaManager/launcher-extra.yml"
DYNAMIC_OPTS=""
# Check if launcher-extra.yml exists
if [ -f "\$LAUNCHER_YML" ]; then
  DYNAMIC_OPTS=\$(${yq-go}/bin/yq '.jvmOpts[] | select(test("^\\[macos\\]") == false) | sub("^\\[linux\\]\\s*", "")' "\$LAUNCHER_YML" | xargs)
  DYNAMIC_CLASSPATH=\$(${yq-go}/bin/yq '.classpath[] | select(test("^\\[macos\\]") == false) | sub("^\\[linux\\]\\s*", "") | sub("^CONTENT_DIR", "~/.local/share/tinyMediaManager") | sub("^/", "")' "\$LAUNCHER_YML" | paste -sd ":" -)
  mapfile -t DYNAMIC_ENV_VARS < <(${yq-go}/bin/yq '.env[] | select(test("^\\[macos\\]") == false) | sub("^\\[linux\\]\\s*", "")' "\$Static_LAUNCHER_YML" 2>/dev/null)
fi

for env_item in "''${STATIC_ENV_VARS[@]}"; do
    if [ -n "\$env_item" ]; then
        export "\$env_item"
    fi
done

for env_item in "''${STATIC_ENV_VARS[@]}"; do
    if [ -n "\$env_item" ]; then
        export "\$env_item"
    fi
done

export JDK_JAVA_OPTIONS="-Djava.library.path=./native \$Static_OPTS \$DYNAMIC_OPTS"

cd $out/opt/tinyMediaManager
exec ${jdk}/bin/java -cp "\$Static_CLASSPATH:\$DYNAMIC_CLASSPATH" org.tinymediamanager.TinyMediaManager "\$@"
EOF
    chmod +x $out/bin/tinyMediaManager

    # Handle the icon
    mkdir -p $out/share/pixmaps
    if [ -f "$out/opt/tinyMediaManager/tmm.png" ]; then
        ln -s $out/opt/tinyMediaManager/tmm.png $out/share/pixmaps/tinyMediaManager.png
    fi

    runHook postInstall

substituteInPlace $out/opt/tinyMediaManager/launcher.yml \
--replace 'jvmOpts:' "jvmOpts:
  - '-Dtmm.noupdate=true'
  - '-Dtmm.usepath=true'"

substituteInPlace $out/opt/tinyMediaManager/launcher.yml \
--replace 'env:' "env:
  - \"_JAVA_AWT_WM_NONREPARENTING=1\""
  '';

  meta = with lib; {
    description = "A media management tool";
    homepage = "https://www.tinymediamanager.org/";
    changelog = "https://gitlab.com/tinyMediaManager/tinyMediaManager/-/releases/tinyMediaManager-${finalAttrs.version}#changelog";
    sourceProvenance = [ lib.sourceTypes.binaryNativeCode ];
    license = licenses.asl20;
    maintainers = with lib.maintainers; [ gamebeaker ];
    platforms = [
      "x86_64-linux"
      "aarch64-linux"
    ];
    mainProgram = "tinyMediaManager";
  };
}

echo -n $(nix-instantiate --eval --expr '((import <nixpkgs> {}).callPackage ./package.nix {}).installPhase')

shows this:

for env_item in \"\${STATIC_ENV_VARS[@]}\"; do
 if [ -n \"\$env_item\" ]; then
 export \"\$env_item\"
 fi
done

for env_item in \"\${STATIC_ENV_VARS[@]}\"; do
 if [ -n \"\$env_item\" ]; then
 export \"\$env_item\"
 fi
done

So the envvar is there correctly. Bash sees this:

for env_item in "${STATIC_ENV_VARS[@]}"; do
 if [ -n "$env_item" ]; then
 export "$env_item"
 fi
done

for env_item in "${STATIC_ENV_VARS[@]}"; do
 if [ -n "$env_item" ]; then
 export "$env_item"
 fi
done

So for me, everythhing in the generated script looks fine.

I uploaded a video: 2026-05-22 23-23-47.mp4 - Storage Share

I thought nixos would prevent “it works on my machine” xD.

I have no clue what file you looked at in that video, though it might have been through additional stages. I just realized that you have a <<EOF…EOF slapped around the area.

So maybe you are missing some bash escaping on top, making it "\''${…}" should break that level.

1 Like

That was it. Huge thanks <3
"\''${DYNAMIC_ENV_VARS[@]}""${STATIC_ENV_VARS[@]}"

Should i use something else?

No, you just have to properly be aware of the various escaping levels you go through, when you embed languages in languages, here bash in bash in nix.

1 Like

Or maaaaybe avoid doing that in the first place. I don’t think there is ever a legitimate reason to doubly-nest bash in ways that would cause quoting issues; use subshells or functions instead, or pkgs.writeText (or pkgs.writers) if you want to create files.

Haven’t seen the code, of course, this just sounds like a wrong choice somewhere a bit earlier.

2 Likes