Skip to main content

Batch file preprocessor

Project description

Pbat

Pbat is batch file preprocessor developed to introduce functions, macro expressions and automate PATH variable management.

Pbat script is compiled into bat file to run localy and (optionaly) into yaml workflow to run on github actions runner.

Functions

Pbat script contains of functions. Each function represents one step of the workflow. Functions introduced by def keyword. Function body consists of shell commands and macro expressions.

functions1.pbat (source)

def main
    echo hello world

functions1.bat (generated)

@echo off
rem This file is generated from functions1.pbat, all edits will be lost
echo hello world

Last defined function become main step.

functions2.pbat (source)

def foo
    echo foo

def bar
    echo bar

functions2.bat (generated)

@echo off
rem This file is generated from functions2.pbat, all edits will be lost
echo bar

To chain steps (prepend them to workflow) add depends on ...name to function definition.

functions3.pbat (source)

def foo
    echo foo

def bar depends on foo
    echo bar

functions3.bat (generated)

@echo off
rem This file is generated from functions3.pbat, all edits will be lost
echo foo
echo bar

Macros

Macro expression consists of name and comma-separated arguments enclosed in parenthesis, strings may be enclosed into double quotes, but it's not required. Arguments can be positional and named, named arguments expressed as :name=value or just :name for boolean true value.

use(program) includes relative paths into PATH variable. Defined for cmake, ninja and some other tools.

download(url, [file], [:cache]) curls specified url into local file, if :cache specified curl is only called if file not exist.

add_path(path) appends path into PATH env variable.

unzip(zip_path, [:test=path/to/file/or/dir], [:output=path/to/dir]) unzips zip_path using 7z, if :test specified 7z is only called if file not exist.

zip(zip_path, [...path], [:test]) zips one or many paths into zip_path.

if_exist_return(path) exits function if path exists.

return() exist function unconditionally.

patch(path, [:p1], [:N]) calls patch.

git_clone(url, [:ref=tag], [:pull]) clones git repo.

macros1.pbat (source)

def main
    use(cmake)
    :: download
    download(http://example.com/foo.zip)
    :: download unless exists
    download(http://example.com/bar.zip, :cache)
    :: unzip
    unzip(foo.zip)
    :: unzip unless bar exists
    unzip(bar.zip, :t=bar)
    :: unzip two files
    unzip(bar.zip, part1.txt, part2.txt)
    time /t > time.txt
    date /t > date.txt
    :: zip two files
    zip(data.zip, time.txt, date.txt)
    mkdir(foo)
    rmdir(bar)
    xcopy(src, dst)
    patch(..\patch.patch, :p1, :N)
    del(part1.txt, part2.txt)

macros1.bat (generated)

@echo off
rem This file is generated from macros1.pbat, all edits will be lost
set PATH=C:\Program Files\CMake\bin;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\mingw32\bin;C:\Windows\System32;C:\Program Files\7-Zip;C:\Program Files\Git\usr\bin;%PATH%
:: download
curl -L -o foo.zip http://example.com/foo.zip
:: download unless exists
if not exist bar.zip curl -L -o bar.zip http://example.com/bar.zip
:: unzip
7z x -y foo.zip
:: unzip unless bar exists
if not exist bar 7z x -y bar.zip
:: unzip two files
7z x -y bar.zip part1.txt part2.txt
time /t > time.txt
date /t > date.txt
:: zip two files
7z a -y data.zip time.txt date.txt
if not exist foo mkdir foo
if exist bar rmdir /s /q bar
xcopy /s /e /y /i src dst
patch -N -p1 -i ..\patch.patch
del /f /q part1.txt part2.txt

There are a number of github_ macros that do nothing for local script but add steps into github actions workflow.

github_checkout()

github_cache(...path, :key=key)

github_upload(path, [:name=name])

github_release(path)

To turn on github workflow generation add github-workflow 1 anywhere in script.

macros2.pbat (source)

def main
    github_checkout()
    time /t > time.txt
    github_upload(time.txt)
    github_release(time.txt)
    
github-workflow 1

macros2.bat (generated)

@echo off
rem This file is generated from macros2.pbat, all edits will be lost
time /t > time.txt

macros2.yml (generated)

name: main
on: push
jobs:
  main:
    runs-on: windows-latest
    steps:
    - name: checkout
      uses: actions/checkout@v4
    - name: main
      shell: cmd
      run: time /t > time.txt
    - name: upload
      uses: actions/upload-artifact@v4
      with:
        name: time
        path: time.txt
    - name: release
      uses: softprops/action-gh-release@v2
      if: startsWith(github.ref, 'refs/tags/')
      with:
        files: time.txt

Lets cache build artifact time.txt using cache action on github workflow and filesystem persistance localy.

macros3.pbat (source)

def generate
    github_cache(time.txt, :k=time)
    if_exist_return(time.txt)
    time /t > time.txt

def main depends on generate
    type time.txt
    
github-workflow 1

macros3.bat (generated)

@echo off
rem This file is generated from macros3.pbat, all edits will be lost
if exist time.txt goto generate_end
time /t > time.txt
:generate_end
type time.txt

macros3.yml (generated)

name: main
on: push
jobs:
  main:
    runs-on: windows-latest
    steps:
    - name: cache time.txt
      uses: actions/cache@v4
      with:
        path: time.txt
        key: time
    - name: generate
      shell: cmd
      run: |
        if exist time.txt goto generate_end
        time /t > time.txt
        :generate_end
    - name: main
      shell: cmd
      run: type time.txt

You can split code into multiple files and use include(path) to put it back together.

dep1.pbat (source)

def dep1
    echo dep1

include1.pbat (source)

include(dep1.pbat)

def main depends on dep1
    echo main

include1.bat (generated)

@echo off
rem This file is generated from include1.pbat, all edits will be lost
echo dep1
echo main

PATH pollution

Some applications change their behaviour depending on rm, bash, sed, etc availability in PATH (qmake for example generates completely different makefiles, although they work as well, it makes me uneasy), to avoid poluting PATH when download or patch is used, you can use env-policy 1 to use %PATCH% and %CURL% env variables containing full path to this utilities.

env_policy1.pbat (source)

def main
    clear_path()
    where curl
    where patch
    where rm
    where bash
    return()
    download(http://example.com/foo.zip)
    patch(test)

env_policy1.bat (generated)

@echo off
rem This file is generated from env_policy1.pbat, all edits will be lost
set PATH=C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\mingw32\bin;C:\Windows\System32;C:\Program Files\Git\usr\bin;C:\Windows
where curl
where patch
where rm
where bash
goto main_end
curl -L -o foo.zip http://example.com/foo.zip
patch -i test
:main_end

env_policy2.pbat (source)

def main
    clear_path()
    where curl
    where patch
    where rm
    where bash
    return()
    download(http://example.com/foo.zip)
    patch(test)

env-policy 1

env_policy2.bat (generated)

@echo off
rem This file is generated from env_policy2.pbat, all edits will be lost
set PATH=C:\Windows;C:\Windows\System32
if exist "C:\Program Files\Git\usr\bin\patch.exe" set PATCH=C:\Program Files\Git\usr\bin\patch.exe
if not defined PATCH (
echo PATCH not found
exit /b
)
if exist "C:\Program Files\Git\mingw32\bin\curl.exe" set CURL=C:\Program Files\Git\mingw32\bin\curl.exe
if exist "C:\Program Files\Git\mingw64\bin\curl.exe" set CURL=C:\Program Files\Git\mingw64\bin\curl.exe
if exist "C:\Windows\System32\curl.exe" set CURL=C:\Windows\System32\curl.exe
if not defined CURL (
echo CURL not found
exit /b
)
where curl
where patch
where rm
where bash
goto main_end
"%CURL%" -L -o foo.zip http://example.com/foo.zip
"%PATCH%" -i test
:main_end

Notes

Identation is optional.

Advanced usage

With pbat you can build advanced readable pipelines consisting of clearly defined reusable blocks.

advanced1.pbat (source)

def install_compiler
    github_cache(C:\compiler, :k=compiler)
    add_path(C:\compiler)
    if_exist_return(C:\compiler\cl.exe)
    download(https://example.com/compiler.zip, :cache)
    unzip(compiler.zip, :o=C:\compiler, :t=C:\compiler\cl.exe)

def build_lib
    use(cmake)
    use(ninja)
    git_clone(https://example.com/lib.git)
    pushd lib
        cmake -D CMAKE_INSTALL_PREFIX=C:/example ..
        cmake --build .
        cmake --install .
    popd

def build_app depends on install_compiler and build_lib
    github_checkout()
    mkdir(build)
    pushd build
        cmake -D CMAKE_PREFIX_PATH=C:/example ..
        cmake --build .
    popd
    zip(app.zip, build\app.exe, C:\example\bin\example.dll)
    github_upload(app.zip)
    github_release(app.zip)

github-workflow 1

advanced1.bat (generated)

@echo off
rem This file is generated from advanced1.pbat, all edits will be lost
set PATH=C:\compiler;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\mingw32\bin;C:\Windows\System32;C:\Program Files\7-Zip;C:\Program Files\CMake\bin;C:\Program Files\Meson;C:\Program Files\Git\cmd;%PATH%
if exist C:\compiler\cl.exe goto install_compiler_end
if not exist compiler.zip curl -L -o compiler.zip https://example.com/compiler.zip
if not exist C:\compiler\cl.exe 7z x -y -oC:\compiler compiler.zip
:install_compiler_end
if not exist lib git clone https://example.com/lib.git
pushd lib
    cmake -D CMAKE_INSTALL_PREFIX=C:/example ..
    cmake --build .
    cmake --install .
popd
if not exist build mkdir build
pushd build
    cmake -D CMAKE_PREFIX_PATH=C:/example ..
    cmake --build .
popd
7z a -y app.zip build\app.exe C:\example\bin\example.dll

advanced1.yml (generated)

name: main
on: push
jobs:
  main:
    runs-on: windows-latest
    steps:
    - name: checkout
      uses: actions/checkout@v4
    - name: cache C:\compiler
      uses: actions/cache@v4
      with:
        path: C:\compiler
        key: compiler
    - name: install_compiler
      shell: cmd
      run: |
        set PATH=C:\compiler;%PATH%
        if exist C:\compiler\cl.exe goto install_compiler_end
        if not exist compiler.zip curl -L -o compiler.zip https://example.com/compiler.zip
        if not exist C:\compiler\cl.exe 7z x -y -oC:\compiler compiler.zip
        :install_compiler_end
    - name: build_lib
      shell: cmd
      run: |
        set PATH=C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja;C:\Program Files (x86)\Android\android-sdk\cmake\3.22.1\bin;C:\Program Files\Git\cmd;%PATH%
        if not exist lib git clone https://example.com/lib.git
        pushd lib
            cmake -D CMAKE_INSTALL_PREFIX=C:/example ..
            cmake --build .
            cmake --install .
        popd
    - name: build_app
      shell: cmd
      run: |
        if not exist build mkdir build
        pushd build
            cmake -D CMAKE_PREFIX_PATH=C:/example ..
            cmake --build .
        popd
        7z a -y app.zip build\app.exe C:\example\bin\example.dll
    - name: upload
      uses: actions/upload-artifact@v4
      with:
        name: app
        path: app.zip
    - name: release
      uses: softprops/action-gh-release@v2
      if: startsWith(github.ref, 'refs/tags/')
      with:
        files: app.zip

Install

pip install pbat

Compile scripts

python -m pbat.compile path/to/file/or/dir

or

pbat path/to/file

Watch and compile

You can use eventloop to trigger pbat on filechange

onchange path\to\dir -i *.pbat -- pbat FILE
onchange path\to\file -- pbat FILE

More examples

antlr4-cpp-demo/build.pbat

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pbat-0.0.25.tar.gz (24.3 kB view details)

Uploaded Source

Built Distribution

pbat-0.0.25-py3-none-any.whl (23.2 kB view details)

Uploaded Python 3

File details

Details for the file pbat-0.0.25.tar.gz.

File metadata

  • Download URL: pbat-0.0.25.tar.gz
  • Upload date:
  • Size: 24.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.5

File hashes

Hashes for pbat-0.0.25.tar.gz
Algorithm Hash digest
SHA256 b60cef8082b3dd68b1bbefa55e751c2771875bf9b3ae8eac18e044f05b7bcbc9
MD5 6df38897609da98d5a62a548dbb4ca63
BLAKE2b-256 23187d07edbc217abeefc45ff18cd732fcbf22acc0ecd75c73133ace7ea70875

See more details on using hashes here.

File details

Details for the file pbat-0.0.25-py3-none-any.whl.

File metadata

  • Download URL: pbat-0.0.25-py3-none-any.whl
  • Upload date:
  • Size: 23.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.5

File hashes

Hashes for pbat-0.0.25-py3-none-any.whl
Algorithm Hash digest
SHA256 3ce98451c83bd6cdfa22737ebeef496108a23b6afca4b88934f3f7282326a9ec
MD5 5e12c13cbb86bdac3608ef69ccf1a397
BLAKE2b-256 a883f9493e0c2056e8eab9fa2f9f5d6b4cdf7283dc828f1e973531a934672ec9

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page