Search code examples
powershellsyntaxreadabilityline-continuation

Line Continuation Mid String In Powershell


I'm trying to write an FFmpeg command on system that has two identical capture cards, so I have to use their "alternate names" or FFmpeg doesn't understand which one I'm calling.

The only problem is the alternate names are extremely long, especially when calling both audio and video via dshow:

-i video="@device_pnp_\\?\usb#vid_07ca&pid_0570&mi_00#7&3886ab1a&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global":audio="@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{0E6F0DEF-2B29-4117-8D30-13F01160AC5B}"

I'd like to break this "string" mid line before calling audio to make it more readable like so:

-i video="@device_pnp_\\?\usb#vid_07ca&pid_0570&mi_00#7&3886ab1a&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global"`
:audio="@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{0E6F0DEF-2B29-4117-8D30-13F01160AC5B}"

However backtick doesn't seem to work in this scenario, I guess because it's bordering a character? If I and a space before the backtick it breaks the string and FFmpeg errors out:

    -i video="@device_pnp_\\?\usb#vid_07ca&pid_0570&mi_00#7&3886ab1a&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global" `
:audio="@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{0E6F0DEF-2B29-4117-8D30-13F01160AC5B}"

Is there anyway to split this across multiple lines in Powershell?

Edit - Full functional command:

ffmpeg -y -hide_banner -thread_queue_size 9999 -indexmem 9999 -guess_layout_max 0 -f dshow -rtbufsize 2147.48M `
-video_size 1920x1080 -framerate 60 `
-i video="@device_pnp_\\?\usb#vid_07ca&pid_0570&mi_00#7&3886ab1a&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global":audio="@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{0E6F0DEF-2B29-4117-8D30-13F01160AC5B}" `
-thread_queue_size 9999 -indexmem 9999 -guess_layout_max 0 -f dshow -rtbufsize 2147.48M `
-video_size 1920x1080 -framerate 60 `
-i video="@device_pnp_\\?\usb#vid_07ca&pid_0570&mi_00#7&24df76f&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global":audio="@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{0A494693-5F33-4304-88D7-394757E09648}" `
-map 0:0,0:1 -map 0:1 -map 1:0,1:1 -map 1:1 -c:v h264_nvenc -r 60 -rc-lookahead 120 -forced-idr 1 -strict_gop 1 `
-sc_threshold 0 -flags +cgop -force_key_frames "expr:gte(t,n_forced*2)" -preset: llhp -pix_fmt yuv420p -b:v 250M `
-minrate 250M -maxrate 250M -bufsize 250M -c:a aac -ar 44100 -b:a 384k -ac 2 -af "aresample=async=250" -vsync 1 `
-max_muxing_queue_size 9999 -f segment -segment_time 600 -segment_wrap 9 -reset_timestamps 1 `
-segment_format_options max_delay=0 C:\Users\Jordan\Videos\FFmpeg\FFmpeg%02d.ts

My goal is to break my calls for video / audio devices to better fit the rest of the block.


Solution

  • Using ` at the end of a line inside a "..." string or between strings in PowerShell does not work the same as using \ for line continuation in Bash would, for instance:

    While ` does enable line continuation in PowerShell if placed at the very end of a line:

    • when used inside a line-spanning "..." string, the ` escapes the newline, without removing it, which is effectively a no-op, given that newlines inside PowerShell string literals don't require escaping.

    • when, as in your attempt, used between strings that should form a single argument (strings that would be placed directly next to each other on a single line, e.g., video="...":audio="..."), the ` effectively acts as an argument separator, so you cannot split what should become a single string that way.[1]

    In short: In PowerShell,

    • You can only use line continuation between arguments.

    • In order to spread a single argument across multiple lines, you need to use an expression ((...)); expressions are allowed to span multiple lines without line continuation.


    One option in your case is to specify the string parts as elements of an array that are later joined to form a single string.

    Using a simplified example; note how the -join technique can coexist with use of line-ending ` to spread the command across multiple line between arguments:

    ffmpeg -framerate 60 `
      -i (@(
        'video=@device_pnp_\\?\1'
        'audio=@device_cm_2'
      ) -join ':') `
      -f dshow
    

    This translates to the following single line on execution:

    ffmpeg -framerate 60 -i video=@device_pnp_\\?\1:audio=@device_cm_2 -f dshow
    

    Note that your original argument-interior quoting (video="...":audio="...") was not used, because it generally isn't necessary (though certain programs, such as msiexec, regrettably require it).

    Alternatively, if you don't mind including the : in each substring but the last, you can use + for string concatenation, as Ted Shaneyfelt's answer points out:

    ffmpeg -framerate 60 `
      -i (
        'video=@device_pnp_\\?\1:' +
        'audio=@device_cm_2'
      ) `
      -f dshow
    

    PowerShell automatically double-quotes arguments for external programs on demand, namely if they contain whitespace that PowerShell thinks is not inside embedded double quotes. See this answer for more, including when this can be problematic.


    Another alternative is to construct (just) the long arguments up front and reference them via a variable:

    $inputSpec = @(
      'video=@device_pnp_\\?\1'
      'audio=@device_cm_2'
    ) -join ':'
    
    ffmpeg -framerate 60 -i $inputSpec -f dshow
    

    To generally spread a command's arguments across multiple lines without having to use ` at the end of the line between arguments, construct the arguments in an array up front, and use splatting to pass it:

    # Construct an array containing all arguments.
    # Note: Each parameter (+ operand, if any) is placed on its own line
    #       here for clarity, but you're free to choose as many or few 
    #       lines desired.
    $ffmpegArgs = @(
      '-y'
      'hide_banner'
      '-thread_queue_size', '9999'
      '-indexmem', '9999'
      '-guess_layout_max', '0'
      '-f', 'dshow'
      '-rtbufsize', '2147.48M'
      '-video_size', '1920x1080'
      '-framerate', '60'
      '-i', (@(
        'video=@device_pnp_\\?\usb#vid_07ca&pid_0570&mi_00#7&3886ab1a&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global'
        'audio=@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{0E6F0DEF-2B29-4117-8D30-13F01160AC5B}'
      ) -join ':')
    )
    
    # ...
        
    # Pass the array via splatting - note the '@'
    # (Since ffmpeg is an *external program*, passing the array
    #  directly, as $ffmpegArgs would work too.)
    ffmpeg @ffmpegArgs
    

    Note:

    • Splatting's primary use case is to enable programmatic construction of a command's argument, and requires different syntax, as shown.

    • This - ultimately rejected - RFC proposal proposed new syntax that would make multi-line continuations with regular argument syntax easier, without needing ` at the end of each interior line; the decision to reject is here.


    [1] For more information on how PowerShell parses compound tokens, see GitHub issue #6467.