Search code examples
bashshellcygwin

printf won't print a variable in Cygwin, though working on Mac OS Terminal


I have the following code snippet in a script:

bitrate=$(ffmpeg -i "$vidfile" 2>&1 | grep bitrate | \
              perl -pe 's/.*bitrate: (\d{1,} .*)\r?\n\r?$/$1/')
echo "\$bitrate = $bitrate" #added for debugging
printf "\$bitrate = $bitrate\n" #added for debugging
printf " $bitrate $vidfile\n" #added for debugging
printf " %s %s \n\n" "$bitrate" "$vidfile"

This BASH shell script (minus the lines added for debugging) was working fine as-is on Mac OS X Terminal. When when I try to run it on Cygwin, the last & 2nd-last printf don't print the value of the $bitrate variable.

Sample output from a run:

$ bitrate.sh 20180305-01.mov 20180305-02.mov
$bitrate = 31103 kb/s
$bitrate = 31103 kb/s
 20180305-01.mov
 20180305-01.mov

$bitrate = 30735 kb/s
$bitrate = 30735 kb/s
 20180305-02.mov
 20180305-02.mov

Solution

  • The problem is due to the nonprinting carriage return character (sometimes represented as \r or ^M or <CR>). DOS and Windows use the convention that lines of text are terminated with a carriage return followed by a linefeed, but unix just uses a linefeed. As a result, when you take a DOS/Windows-format line and hand it to a unix program (e.g. the shell), it thinks the linefeed is the end of the line, and treats the carriage return as part of the content of the line. This problem happens a lot when using unix tools in Windows (e.g. Cygwin).

    In this case, I assumed it was because the script file itself was in DOS/Windows format, but apparently not; so I'm not sure exactly where the carriage return is coming from (perhaps perl is trying to be Windows-compatible and outputting DOS/Windows format?). Anyway, the net result is that the variable bitrate has a carriage return at the end. Printing it by itself, this isn't visible, but if you print it followed by a space and the filename, it sends "31103 kb/s\r 20180305-01.mov" to the terminal, which prints "31103 kb/s", then the carriage return makes it go back to the beginning of the line (that's actually what "carriage return" means"), then it prints " 20180305-01.mov" over top of the "31103 kb/s", replacing it.

    Solution: remove the carriage return, by adding tr -d '\r' to the end of the pipeline calculating the bitrate.

    Useful troubleshooting tricks when you suspect a problem like this: print variables with printf's %q format:

    $ printf 'the var is %q\n' "$bitratenormal"
    the var is 31103\ kb/s
    $ printf 'the var is %q\n' "$bitratewithcr"
    the var is $'31103 kb/s\r'
    

    ...and run files through LC_ALL=C cat -vet to make a variety of normally-invisible characters visible:

    $ cat filewithweirdchars 
    This line is normal
    This line ends with a space 
    This line ends with a tab   
    This line ends with a carriage return
    This line has nonbreaking spaces and “weird” unicode quotes
    $ LC_ALL=C cat -vet filewithweirdchars 
    This line is normal$
    This line ends with a space $
    This line ends with a tab^I$
    This line ends with a carriage return^M$
    ThisM-BM- lineM-BM- hasM-BM- nonbreakingM-BM- spaces and M-bM-^@M-^\weirdM-bM-^@M-^] unicode quotes$