sh Python Library

repository·develop·Indexed 27 days ago

https://github.com/amoffat/sh

A subprocess replacement for Python (version 2.4.0) that allows developers to call any system program in their $PATH as if it were a native Python function. It features dynamic binary resolution, support for asyncio, background process execution via _bg=True, and the ability to pre-apply arguments using .bake(). The library provides a Command class for program representation and a RunningCommand class for process interaction, including output redirection, piping, and custom callbacks for incremental processing.

Tokens
14.3K
Snippets
57
Records
105
Agent score
90%

What's inside sh

  1. Pass multiple arguments to commands correctly

    develop

    When calling a command with multiple arguments, each argument must be passed as a separate string. Do not combine multiple arguments into a single string, as this will fail to execute the command correctly.

    from sh import tar
    # Correct: each argument is a separate string
    tar("cvf", "/tmp/test.tar", "/my/home/directory/")
    
    # Incorrect: this will not work
    tar("cvf /tmp/test.tar /my/home/directory")
  2. Configure /etc/sudoers NOPASSWD for sh.sudo

    develop

    If you configure your system's /etc/sudoers file to allow your user to run specific programs without a password using NOPASSWD, you can use the raw sh.sudo command directly without handling password input in Python.

    To configure this, run sudo visudo and add a line similar to: yourusername ALL = (root) NOPASSWD: /path/to/your/program.

    Warning: This can be insecure if an unprivileged user can edit the script being executed with these privileges.

    $> sudo visudo
    
    # Add to sudoers:
    yourusername ALL = (root) NOPASSWD: /path/to/your/program
  3. View commands executed by sh using logging

    develop

    To inspect the commands that sh is running, configure the standard Python logging module with a level of logging.INFO. This will output details about process starting, PID assignment, and completion.

    import logging
    import sh
    
    logging.basicConfig(level=logging.INFO)
    sh.ls()
  4. Configure process session behavior with _new_session

    develop

    In sh 2.x, the _new_session keyword argument now defaults to False. This ensures launched processes stay in the same process group as the Python script, allowing them to receive signals like SIGINT correctly.

    If you require the legacy behavior where every process launches in a new session, set _new_session=True via sh.bake().

    import sh
    
    # Restore 1.x behavior where processes launch in new sessions
    sh = sh.bake(_new_session=True)
  5. Run commands within a 'with' context

    develop

    You can execute commands inside a Python with context using specific command contributions. This is useful for commands like sudo or fakeroot that modify the execution environment.

    Note: When using a with context that requires arguments (such as passing a prompt flag to sudo), you must pass _with=True to the command to ensure it behaves correctly within the context.

    # Basic usage
    with sh.contrib.sudo:
        print(ls("/root"))
    
    # Usage with arguments (requires _with=True)
    with sh.contrib.sudo(k=True, _with=True):
        print(ls("/root"))
  6. Merge new environment variables with existing environment

    develop

    To add new environment variables to a command without losing your current process's environment, you must manually copy os.environ and update the copy before passing it to the _env argument.

    import os
    import sh
    
    new_env = os.environ.copy()
    new_env["SOCKS_SERVER"] = "localhost:1234"
    
    sh.google_chrome(_env=new_env)
  7. Perform basic piping using function composition

    develop

    In sh, Bash-style piping is achieved through function composition. You can pass one command as the input to another command's _in argument. By default, this behavior is synchronous: the inner command will block and complete entirely before its output is sent to the outer command.

    # sort this directory by biggest file
    print(sort("-rn", _in=du(glob("*"), "-sb")))
    
    # print the number of folders and files in /etc
    print(wc("-l", _in=ls("/etc", "-1")))
  8. Bake arguments into commands using .bake()

    develop
    You can use the .bake() method to perform partial application on a command. This allows you to pre-specify certain arguments so that every subsequent call to the resulting object includes those arguments automatically. This is useful for creating specialized versions of common commands.
  9. Execute remote commands via SSH using baking and subcommands

    develop

    Instead of automating password entry, the recommended way to use SSH with sh is to use ssh-copy-id to set up key-based authentication. Once configured, you can use sh.ssh.bake() to create a reusable command object for a specific server.

    By combining bake with sh's subcommands feature, you can call remote commands as if they were local methods on the baked object.

    import sh
    
    # Create a reusable command object for a specific host
    my_server = sh.ssh.bake("amoffat@10.10.10.100")
    
    # Use subcommands to call remote binaries directly
    print(my_server.ifconfig())
    print(my_server.whoami())
  10. Use callbacks for event-driven output processing with _out

    develop

    To achieve a truly event-driven pattern without blocking your main execution thread, use the _out special keyword argument to assign a callback function to STDOUT. When combined with _bg=True (to run the process in the background), the callback will be invoked for each line of output produced by the command.

    from sh import tail
    
    def process_log_line(line):
        if "ERROR" in line:
            send_an_email_to_support(line)
    
    # _out assigns the callback to STDOUT, _bg runs it in the background
    process = tail("-f", "info.log", _out=process_log_line, _bg=True)
    
    # ... do other stuff here ...
    
    process.wait()
  11. Handle command exit codes and exceptions

    develop

    By default, sh commands return an exit code of 0 for successful processes. You can access this via the exit_code attribute on the returned object when using _return_cmd=True.

    If a process returns a non-zero exit code, sh dynamically generates an exception. You can catch specific error codes using classes named ErrorReturnCode_<number> or catch all non-zero exit codes using the base class ErrorReturnCode.

    # Accessing exit code
    output = ls("/", _return_cmd=True)
    print(output.exit_code) # should be 0
    
    # Catching specific and general error codes
    try:
        print(ls("/some/non-existent/folder"))
    except ErrorReturnCode_2:
        print("folder doesn't exist!")
    except ErrorReturnCode:
        print("unknown error")