"command not found" 在 OSX 中执行 TCL 脚本时出错

"command not found" errors when executing TCL script in OSX

我正在尝试执行 this TCL script,它应该批量规范化一个 mp3 文件夹。我在 OSX Yosemite(已完全安装 ffmpeg)。该行是:

./normalise.tcl mp3folder

哪个(包括 sudo)returns:

./normalise.tcl: line 37: proc: command not found
./normalise.tcl: line 38: global: command not found
./normalise.tcl: line 40: switch: command not found
./normalise.tcl: line 42: puts: command not found
./normalise.tcl: line 43: syntax error near unexpected token `}'
./normalise.tcl: line 43: `        }'

.. 然后将 shell 定向到内置帮助文档。我对这种语言没有任何经验,所以正在阅读,但到目前为止还没有遇到任何可以解释它的事情。非常感谢知道出了什么问题。


编辑

脚本注释中建议的 -d 选项似乎无效。

完整脚本:

#!/bin/sh
#\
exec tclsh "[=14=]" ${1+"$@"}

# Copyright 2015 Tholis Biroi (tholis DOT biroi AT yahoo DOT it)
#
# This file is part of 'normalize.tcl'.
# 'normalize.tcl' is free software: you can redistribute it and/or modify 
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or 
# (at your option) any later version.
# 'normalize.tcl' is distributed in the hope that it will be useful, but 
# WITHOUT ANY WARRANTY; without even the implied warranty of 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
#
# See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License 
# along with 'normalize.tcl'. If not, see http://www.gnu.org/licenses/.
#
#
#
# 'normalize.tcl' is a simple TCL script that drives 'ffmpeg' to normalise
# audio levels for a group of mp3 files.
# For each mp3 file found into a directory, it reads the mean volume level, 
# calculates the average level among various files and adjusts the volume
# level of each of them.
#

# Global debugging variable
# To override this option set '-d' on the command line
set debugOn false

# log --
#
# puts "" wrapper 
#
proc log {label args} {
    global debugOn

    switch $label {
        "info" {
            puts {*}$args
        }
        "error" {
            puts "error: [join {*}$args]" 
        }
        "warning" {
            puts "warning: [join {*}$args]" 
        }
        "debug" {
            if {$debugOn == "true"} {
                puts "debug: [join {*}$args]" 
            }
        }
        default {
            # do nothing in any case
        }
    }
}


# get_volumes --
#
# Exec 'ffmpeg' in order to get the volume mean level
#
#
proc get_volume {mp3} {
    if {$mp3 == {}} {
        log error "Empty file name."
        return {}
    }

    # Set volume variable
    set volume {}

    # Set 'ffmpeg' command
    set cmd "ffmpeg  -i \"$mp3\" -af \"volumedetect\" -f null /dev/null"    
    log debug "'ffmpeg' cmd= $cmd"

    # Exec 'ffmpeg'
    if {[catch {eval exec -ignorestderr $cmd 2>@1} out]} {
        log error "'ffmpeg' execution command failed."
        log debug "reason= $out"
        return {}
    }

    # In order to avoid 'case sensitive' parsing, the output of the 
    # command is converted to uppercase
    set Out [string toupper $out]    
    log debug "'ffmpeg' out= $Out"

    # Now scan the out a line at time searching for 'MEAN_VOLUME:' 
    # output string label
    set lines [split $Out "\n"]

    foreach line $lines {
        log debug "$line"
        # first of all search for 'VOLUMEDETECT' string and if foud
        # search for 'MEAN_VOLUME:' string.
        if {[string first VOLUMEDETECT $line] == -1} {
            # Not found, skip line parsing 
            continue
        }

        # 'VOLUMEDETECT' string found, search for 'MEAN_VOLUME' string
        set pos [string first MEAN_VOLUME $line]
        if { $pos != -1} {
            set start [expr {$pos + 11}]
            set volStr [string range $line $start end]
            log debug "volStr= $volStr"

            # Extract and trim the first word as volume
            set words [split $volStr]
            log debug "words= $words"
            set volume [string trim [lindex $words 1]]
            log debug "volume= $volume"
        }
    }

    return $volume
} ;# end get_volume


# set_volume --
#
# Exec 'ffmpeg' to re-encode the mp3
#
proc set_volume {mp3 actualVol targetVol} {
    if {($mp3 == {}) || ($actualVol == {}) || ($actualVol == {})} {
        log error "One or more parameter are empty"
        return {}
    }

    # Create filename output
    set mp3Root [file rootname $mp3]
    set mp3OutFile "${mp3Root}.norm.mp3"

    # If normalized file already exists, will be deleted
    if {[file exists $mp3OutFile]} {
        catch {file delete -force -- $mp3OutFile}
    }

    # calculate the delta volume
    set deltaVol [expr {$targetVol - $actualVol}]

    # Set 'ffmpeg' command
    set cmd "ffmpeg -y -i \"$mp3\"  -af \"volume=${deltaVol}dB\" \"$mp3OutFile\""

    # Exec 'ffmpeg'
    if {[catch {eval exec -ignorestderr $cmd 2>@1} out]} {
        log error "'ffmpeg' execution command failed."
        log debug "reason= $out"
        return {}
    }   

    # For debug purposes
    set Out [string toupper $out]    
    log debug "'ffmpeg' out= $Out"

    return $deltaVol
} ;# end set_volume


# byebye --
#
proc byebye {} {
    log info ""
    log info "Bye!"
    log info ""
} ;# end byebye


# print_help --
# 
# Prints a little help
#
proc print_help {} {
    global argv0

    log info ""
    log info "Usage: $argv0 \[-h|--help\]|<mp3 dir>"
    log info ""
    log info "-h|--help  Print thi help"
    log info "<mp3 dir>  Directory path containing mp3 to normalize"
    log info ""

    byebye
    return
} ;# end print_help

# Main -----------------------------------------------------------------
log info ""
log info "MP3 normalizer v0.9 21 mar 2015"
log info ""
log info ""

# Save current dir
set currDir [pwd]
log debug "Working dir= $currDir"

# Control input parameters to setup working dir 
# If no parameter is passed a little help is printed on screen
if {$argc == 0} {
    print_help
    exit 0
}

# If more than one parameter is passed
if {$argc != 1} {
    log error "Wrong number of arguments."
    log error "Use '-h' or '--help' option to print usage info."

    byebye
    exit 1
}

# If only one paramter is passed, it could be the help option or the
# desired working path
if {([lindex $argv 0] == "-h") || ([lindex $argv 0] == "--help")} {
    print_help
    exit 0
}

# Save the passed workDir in order to make some controls
set workDir [lindex $argv 0]

# The path passed must be a directory path
if { ![file isdirectory $workDir] } {
    log error "The argument passed is not a valid directory path"

    byebye
    exit 1
}

# The argument passed must be an existing directory
if { ![file exists $workDir] } {
    log error "Directory '$workDir' does not exists"

    byebye
    exit 1
}

# Move on working dir
cd $workDir

# Get the list of files in the current directory
set mp3Files [glob -nocomplain *.mp3]

if {$mp3Files == {}} {
    log info "No .mp3 files found on working dir: '$workDir'"

    byebye
    exit 1
}

# Exclude from this list files with exetension *.norm.mp3"
set mp3FileList {}
foreach mp3 $mp3Files {
    set rootFname [file rootname $mp3]
    set ext [file extension $rootFname]

    if {$ext == ".norm"} {
        # Skip already normalized files from mp3 list
        continue
    }

    lappend mp3FileList $mp3
}


# Init the mp3 array
#set mp3Ar {}

log info "List of file mp3 to be normalized:"

# Foreach *.mp3 file 
foreach mp3 $mp3FileList {
    log info "   '$mp3'"

    # Extract volumes
    set vol [get_volume $mp3]
    if {$vol == {}} {
        log warning "No volume information found for file: $mp3"
    } else {
        # Fill the array of volumes
        set mp3Ar($mp3) $vol
    }
}

log info ""

# parray only for debugging
#parray mp3Ar

# Calculating the average volume
set avgVol 0
set mp3List  [array names mp3Ar]
set numFiles [llength $mp3List]

foreach mp3 $mp3List {
    set avgVol [expr {$mp3Ar($mp3) + $avgVol}]
}
set avgVolume [expr {$avgVol/double($numFiles)}]
set avgVol [format "%0.1f" $avgVolume]

log info "Avg Volume= $avgVol"
log info ""

# Now foreach file calculate delta volume to normalize it
log info "File normalization at $avgVol dB"
foreach mp3 $mp3List {
    log info "    '$mp3' from $mp3Ar($mp3) to $avgVol"
    if {[set_volume $mp3 $mp3Ar($mp3) $avgVol] == {}} {
        log info "warning: Set volume failed for file '$mp3'"
    }
}
log info ""
log info "Done."

# Before exit return to run dir
cd $currDir

byebye

exit 0

出于某种原因,该脚本实际上是由 bourne shell (/bin/sh) 完整执行的,而不是 Tcl。由于这两种语言的语法截然不同,因此您会收到这些错误消息。

但是为什么会这样呢?

嗯,关键是这些:

#!/bin/sh
#\
exec tclsh "[=10=]" ${1+"$@"}

这应该 运行 最初使用 bourne shell 的脚本,然后将执行转移到 Tcl(因为标准 shell exec 替换 当前进程可执行)。它基于这样一个事实,即 shell 并不认为注释中一行末尾的反斜杠是特殊的,但 Tcl 将其视为意味着下一行也是注释的一部分。不同的规则。

然而那是失败的。我猜问题是 tclsh 不在你的路径上(真的吗?它是我的 OSX 系统的标准部分。)所以 exec 失败了并且因此正在解释脚本的其余部分。这有点奇怪。你说你要通过 sudo 所以这可能是个问题,但 sudo 放在 PATH 上的目录之一中确实应该有一个 tclsh默认情况下 (/usr/bin),所以这可能不是正在发生的事情。

此时推荐的方法是更改​​脚本的这三行,以使用单行的更现代的习惯用法:

#!/usr/bin/env tclsh

您也可以在其中使用 tclsh 的完整指定名称。或者您可以尝试使用以下代码启动代码:

tclsh ./normalise.tcl mp3folder

最后一步也是检测是否存在其他问题的好方法;它会覆盖脚本解释器的查找,让您专注于之后发生的事情。