【问题标题】:Wrapping bash scripts in python在 python 中包装 bash 脚本
【发布时间】:2013-12-05 07:01:59
【问题描述】:

我刚刚发现了这个很棒的 wget 包装器,我想使用 subprocess 模块将它重写为 python 脚本。然而,给我各种各样的错误结果是相当棘手的。

download()
{
    local url=$1
    echo -n "    "
    wget --progress=dot $url 2>&1 | grep --line-buffered "%" | \
    sed -u -e "s,\.,,g" | awk '{printf("\b\b\b\b%4s", $2)}'

    echo -ne "\b\b\b\b"
    echo " DONE"
}

那么可以这样调用:

file="patch-2.6.37.gz"
echo -n "Downloading $file:"
download "http://www.kernel.org/pub/linux/kernel/v2.6/$file"

有什么想法吗?

来源:http://fitnr.com/showing-file-download-progress-using-wget.html

【问题讨论】:

  • 您需要了解我们在 Python 中的尝试,以便我们能够为您提供帮助。
  • 基本上还没有..!我目前迷失在子流程文档中..!理想的做法是对提议的解决方案进行有见地的解释,以便我能够正确掌握子流程模块的概念并对其进行扩展。
  • 好吧,到目前为止我做到了:wgetExecutable = '/usr/bin/wget' grepExecutable = '/usr/grep' wgetParameters = ['--progress=dot', "link_to_file"] grepParameters = ['--line-buffered', "%"] wgetPopen = subprocess.Popen([wgetExecutable] + wgetParameters, stdout=subprocess.PIPE)
  • grepPopen = subprocess.Popen([grepExecutable] + grepParameters, stdin=wgetPopen.stdout) 但是我在stdin=wgetPopen.stdout 收到错误 OSError: [Errno 2] No such file or directory
  • 请注意,还有一个sh 模块(具有该名称)可以处理 bash 和 python 之间的桥梁!

标签: python bash shell subprocess wrapper


【解决方案1】:

我认为你离你不远了。主要是我想知道,既然可以在 Python 内部完成所有这些工作,为什么还要费心在 grepsedawk 中运行管道?

#! /usr/bin/env python

import re
import subprocess

TARGET_FILE = "linux-2.6.0.tar.xz"
TARGET_LINK = "http://www.kernel.org/pub/linux/kernel/v2.6/%s" % TARGET_FILE

wgetExecutable = '/usr/bin/wget'
wgetParameters = ['--progress=dot', TARGET_LINK]

wgetPopen = subprocess.Popen([wgetExecutable] + wgetParameters,
                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

for line in iter(wgetPopen.stdout.readline, b''):
    match = re.search(r'\d+%', line)
    if match:
        print '\b\b\b\b' + match.group(0),

wgetPopen.stdout.close()
wgetPopen.wait()

【讨论】:

  • 确实如此。尝试使用较小的文件。或者再等一会儿。 :-)
  • 您的代码似乎会以某种间隔更新,例如在此文件中,第一个进度指示仅在 25% 之后。但是,我需要从一开始就立即取得进展,就像 bash 脚本一样..!
  • 在我的机器上,此脚本的行为与您发布的 bash 脚本的行为相同。它们都以相同的速率产生行缓冲输出。我很乐意调整脚本来做一些不同的事情,但我无法重现你所说的行为。我怀疑您只是看到不同文件的响应时间不同。
  • 啊:如果我在 bash 脚本中使用 awk -W interactive,我得到的结果更接近于您的描述。稍后我会再讨论一下,看看我是否需要做一些特别的事情来强制subprocess 中的行缓冲输出。
  • +1。 wgetPopen.stdout 可能会被破坏(我希望如此,但我不知道)。与普通文件一样,最好明确地关闭它们(with-statement 用于文件)而不依赖垃圾收集(这很复杂且难以推理)。 if not obj 表示“如果 obj 为空或为零”(None 的测试应写为 if obj is None),而与类型无关,例如,在 Python 3 中,pipe.readline() 可能返回不同的 b'''' types 和 if not line 两者都适用。它支持来自同一来源的 Python 2/3。
【解决方案2】:

如果你用 Python 重写脚本;在这种情况下,您可以将 wget 替换为 urllib.urlretrieve()

#!/usr/bin/env python
import os
import posixpath
import sys
import urllib
import urlparse

def url2filename(url):
    """Return basename corresponding to url.

    >>> url2filename('http://example.com/path/to/file?opt=1')
    'file'
    """
    urlpath = urlparse.urlsplit(url).path  # pylint: disable=E1103
    basename = posixpath.basename(urllib.unquote(urlpath))
    if os.path.basename(basename) != basename:
        raise ValueError  # refuse 'dir%5Cbasename.ext' on Windows
    return basename

def reporthook(blocknum, blocksize, totalsize):
    """Report download progress on stderr."""
    readsofar = blocknum * blocksize
    if totalsize > 0:
        percent = readsofar * 1e2 / totalsize
        s = "\r%5.1f%% %*d / %d" % (
            percent, len(str(totalsize)), readsofar, totalsize)
        sys.stderr.write(s)
        if readsofar >= totalsize: # near the end
            sys.stderr.write("\n")
    else: # total size is unknown
        sys.stderr.write("read %d\n" % (readsofar,))

url = sys.argv[1]
filename = sys.argv[2] if len(sys.argv) > 2 else url2filename(url)
urllib.urlretrieve(url, filename, reporthook)

例子:

$ python download-file.py http://example.com/path/to/file 

它将 url 下载到文件中。如果文件没有给出,那么它使用来自 url 的 basename。

如果需要,您也可以运行 wget

#!/usr/bin/env python
import sys
from subprocess import Popen, PIPE, STDOUT

def urlretrieve(url, filename=None, width=4):
    destination = ["-O", filename] if filename is not None else []
    p = Popen(["wget"] + destination + ["--progress=dot", url],
              stdout=PIPE, stderr=STDOUT, bufsize=1) # line-buffered (out side)
    for line in iter(p.stdout.readline, b''):
        if b'%' in line: # grep "%"
            line = line.replace(b'.', b'') # sed -u -e "s,\.,,g"
            percents = line.split(None, 2)[1].decode() # awk $2
            sys.stderr.write("\b"*width + percents.rjust(width))
    p.communicate() # close stdout, wait for child's exit
    print("\b"*width + "DONE")

url = sys.argv[1]
filename = sys.argv[2] if len(sys.argv) > 2 else None
urlretrieve(url, filename)

我没有注意到这段代码有任何缓冲问题。

【讨论】:

    【解决方案3】:

    我以前做过类似的事情。我很乐意与您分享我的代码:)

    #!/usr/bin/python2.7
    # encoding=utf-8
    
    import sys
    import os
    import datetime
    
    SHEBANG = "#!/bin/bash\n\n"
    
    def get_cmd(editor='vim', initial_cmd=""):
        from subprocess import call
        from tempfile import NamedTemporaryFile
        # Create the initial temporary file.
        with NamedTemporaryFile(delete=False) as tf:
            tfName = tf.name
            tf.write(initial_cmd)
        # Fire up the editor.
        if call([editor, tfName], shell=False) != 0:
            return None
            # Editor died or was killed.
            # Get the modified content.
        fd = open(tfName)
        res = fd.read()
        fd.close()
        os.remove(tfName)
        return res
    
    def main():
        initial_cmd = "wget " + sys.argv[1]
        cmd  = get_cmd(editor='vim', initial_cmd=initial_cmd)
        if len(sys.argv) > 1 and sys.argv[1] == 's':
            #keep the download infomation.
            t = datetime.datetime.now()
            filename = "swget_%02d%02d%02d%02d%02d" %\
                    (t.month, t.day, t.hour, t.minute, t.second)
            with open(filename, 'w') as f:
                f.write(SHEBANG)
                f.write(cmd)
                f.close()
                os.chmod(filename, 0777)
        os.system(cmd)
    
    main()
    
    
    # run this script with the optional argument 's'
    # copy the command to the editor, then save and quit. it will 
    # begin to download. if you have use the argument 's'.
    # then this script will create another executable script, you 
    # can use that script to resume you interrupt download.( if server support)
    

    所以,基本上,你只需要修改initial_cmd的值,在你的情况下,它是

    wget --progress=dot $url 2>&1 | grep --line-buffered "%" | \
        sed -u -e "s,\.,,g" | awk '{printf("\b\b\b\b%4s", $2)}'
    

    此脚本将首先创建一个临时文件,然后将 shell 命令放入其中,并赋予它执行权限。最后运行带有命令的临时文件。

    【讨论】:

    • 我很想给你一些反馈:) 你可以call(filename) 而不是os.system(cmd)。要格式化日期时间,您可以使用.strftime() 方法。 with-statement 自动关闭文件,这是首先使用它的重点,无需手动调用 f.close()(在这种情况下不缩进 chmod)。如果你想让你的用户可以执行脚本:os.chmod(filename, os.stat(filename).st_mode | stat.S_IEXEC)(或| 0111 for +x)。为避免文件泄漏,请将代码移动到with Named..File() as tf: 中,在call([editor..) 之前调用tf.flush(),然后在tf.seek(0); res=tf.read() 之前调用tf.seek(0); res=tf.read()
    • @J.F.Sebastian 哇,谢谢你,伙计!这是我很久以前写的脚本。那时我是一个糟糕的 Python 程序员:)谢谢你指出这一点!
    【解决方案4】:

    vim 下载.py

    #!/usr/bin/env python
    
    import subprocess
    import os
    
    sh_cmd = r"""
    download()
    {
        local url=$1
        echo -n "    "
        wget --progress=dot $url 2>&1 |
            grep --line-buffered "%"  |
            sed -u -e "s,\.,,g"       |
            awk '{printf("\b\b\b\b%4s", $2)}'
    
        echo -ne "\b\b\b\b"
        echo " DONE"
    }
    download "http://www.kernel.org/pub/linux/kernel/v2.6/$file"
    """
    
    cmd = 'sh'
    p = subprocess.Popen(cmd, 
        shell=True,
        stdin=subprocess.PIPE,
        env=os.environ
    )
    p.communicate(input=sh_cmd)
    
    # or:
    # p = subprocess.Popen(cmd,
    #    shell=True,
    #    stdin=subprocess.PIPE,
    #    env={'file':'xx'})
    # 
    # p.communicate(input=sh_cmd)
    
    # or:
    # p = subprocess.Popen(cmd, shell=True,
    #    stdin=subprocess.PIPE,
    #    stdout=subprocess.PIPE,
    #    stderr=subprocess.PIPE,
    #    env=os.environ)
    # stdout, stderr = p.communicate(input=sh_cmd)
    

    然后你可以这样调用:

    file="xxx" python dowload.py
    

    【讨论】:

    • 为什么使用sh作为命令,使用shell=True?为什么不直接运行sh_cmd
    • @MartijnPieters 因为 sh_cmd 不是“shell 命令”,所以我们使用sh 来运行它。在linux shell中,我们可以使用sh script.sh,也可以使用PIPE或者stdin来运行一些命令,比如:cat some_file | sh或者curl http://xxx.xx | sh等等。对于shell=Ture,来自文档,是说:shell 参数(默认为False)指定是否使用shell 作为程序来执行。如果 shell 为 True,建议将 args 作为字符串而不是序列传递。
    • 如果您设置了shell=True,则使用一个shell 来运行您传入的命令。您在那里自己引用了文档。
    【解决方案5】:

    简单来说,考虑到你有script.sh文件,你可以执行它并打印它的返回值,如果有的话:

    import subprocess
    process = subprocess.Popen('/path/to/script.sh', shell=True, stdout=subprocess.PIPE)
    process.wait()
    print process.returncode
    

    【讨论】:

    • 并确保script.sh具有执行权限(chmod +x script.sh)或Popen('sh /path/to/script.sh', shell=True ...)
    • 当然,它必须有一个执行权限+X,否则会报错,那么,上面的python代码应该可以运行了!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-01-08
    • 2011-05-24
    • 2013-02-08
    • 1970-01-01
    • 1970-01-01
    • 2018-02-24
    • 1970-01-01
    相关资源
    最近更新 更多