Search code examples
javaprocessbuilder

Java ProcessBuilder Calling Terminal Function with Nested Quotes?


I'm on Linux and am writing a subroutine to change the ID tag of an mp3 file.

This command works: kid3-cli -c "tag 1" -c "set Title "TITLE"" -c "set Artist "ARTIST"" FILENAME.EXT

My attempt at trying this in ProcessBuilder:

new ProcessBuilder(Arrays.asList("kid3-cli", "-c", "\"tag", "1\"", "-c", "\"set", "Title", "\"" + title + "\"\"",
                                    "-c", "\"set", "Artist", "\"" + artist + "\"\"", path.toString()))

I also thought to treat the quotes as a single parameter:

new ProcessBuilder(Arrays.asList("kid3-cli", "-c", "\"tag 1\"", "-c", "\"set Title \"" + title + "\"\"",
                                    "-c", "\"set Artist \"" + artist + "\"\"", path.toString())

Whether or not it was successful can be verified with:

kid3-cli -c "get" SONG.EXT

When I run the program (with a few helpful debugging statements, I get:

INFO: COMMAND: kid3-cli -c "tag 1" -c "set Title "Unfinished Cathedral"" -c "set Artist "DAN TERMINUS"" /home/sarah/Music/Indexing/Temp/DAN TERMINUS - Unfinished Cathedral.mp3

And the tags come up as:

(base) sarah@MidnightStarSign:~/Music/Indexing/Temp$ kid3-cli -c "get" DAN\ TERMINUS\ -\ Unfinished\ Cathedral.mp3 
File: MPEG 1 Layer 3 128 kbps 44100 Hz Joint Stereo 4:38
  Name: DAN TERMINUS - Unfinished Cathedral.mp3
Tag 2: ID3v2.3.0
  Title                   
  Artist                  
  Album                   
  Comment                 Visit http://dan-terminus.bandcamp.com
  Date                    2014
  Track Number            9
  Album Artist            DAN TERMINUS
  Picture: Cover (front)  cover

I'm fairly certain that I'm doing something wrong with regards to the number of entries to give the processbuilder, but I'm not sure. How can I get it to behave properly?

EDIT 0:

Strangely enough,. this also fails (although it has the exact same output when the command is queried, and if run on bash, will change the tags):

new ProcessBuilder(Arrays.asList("bash", "-c", "\"kid3-cli -c \"tag 1\" -c \"set Title \"" + title + "\"\" -c \"set Artist \""
                                + artist + "\"\"" + path.toString() + "\"");

Although it leads to the correct string: INFO: COMMAND: bash -c "kid3-cli -c "tag 1" -c "set Title "Unfinished Cathedral"" -c "set Artist "DAN TERMINUS""/home/sarah/Music/Indexing/Temp/DAN TERMINUS - Unfinished Cathedral.mp3"


Solution

  • There are two problems here:

    • Your command is not doing what you think it’s doing.
    • Double-quotes are appropriate for shells (like bash), not for direct execution in programs.

    First, let’s look at the command-line version of your command:

    This command works: kid3-cli -c "tag 1" -c "set Title "TITLE"" -c "set Artist "ARTIST"" FILENAME.EXT

    Yes, it “works,” but not for the reasons you think. Nesting quotes inside quotes doesn’t just work magically. It would be nearly impossible for any parser to know which quote characters are the end of the argument and which ones are part of the argument.

    What you’re really doing is this:

    kid3-cli -c "tag 1" -c "set Title "TITLE"" -c "set Artist "ARTIST"" FILENAME.EXT
    ╰──┬───╯ ╰┬╯ ╰─┬─╯ ╰┬╯  ╰──────┬── ────╯   ╰┬╯ ╰────────┬─ ─────╯   ╰────┬─────╯
      arg0  arg1  arg2 arg3       arg4        arg5        arg6             arg7
    (program
      path)
    

    As you can see, each quoted argument is one argument. Which means you want to pass each quoted argument as a single parameter to ProcessBuilder, without those quotes. The double-quote characters are only special in a shell (like bash), where they indicate that a value with spaces should not be split by the shell into multiple arguments.

    But there is more. When you do this:

    "set Title "TITLE""
    

    you are not specifying an argument with double-quotes in the argument. All of those double-quote characters are interpreted by the shell, so none of them will be passed to the kid3-cli program.

    What actually is happening is you are concatenating a quoted string, an unquoted title, and then an empty quoted string, all into a single command-line argument. These are all identical:

    "set Title "TITLE""
    "set Title "TITLE
    'set Title 'TITLE
    set\ Title\ TITLE
    

    So, those double-quotes were never being passed to the program at all.

    ProcessBuilder doesn’t need any quotes, because it is not a shell; rather, it directly executes a program with the given arguments. You are already telling ProcessBuilder what constitutes a single argument to the program, simply by passing each such argument as a separate String parameter value.

    (Side note: Arrays.asList is unnecessary. ProcessBuilder has a constructor that takes string arguments directly.)

    new ProcessBuilder("kid3-cli",
        "-c", "tag 1",
        "-c", "set Title \"" + title + "\"",
        "-c", "set Artist \"" + artist + "\"",
        path.toString())
    

    The kid3 man page seems to prefer that you use single quote characters for arguments to kid3 commands, which may make things a little easier to read and a little less confusing:

    new ProcessBuilder("kid3-cli",
        "-c", "tag 1",
        "-c", "set Title '" + title + "'",
        "-c", "set Artist '" + artist + "'",
        path.toString())