diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e7f5b58 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,34 @@ +language: python + +sudo: false +sudo: required + +env: + matrix: + - LUA="lua=5.1" + - LUA="lua=5.2" + - LUA="lua=5.3" + - LUA="luajit=2.0" + - LUA="luajit=2.1" + +branches: + only: + - master + +before_install: + - pip install hererocks + - hererocks base -r^ --$LUA + - export PATH=$PATH:$PWD/base/bin + - sudo add-apt-repository ppa:duggan/bats --yes + - sudo apt-get update -qq + - sudo apt-get install -qq bats + - luarocks install luafilesystem + +script: + - cd test + - bats . + +notifications: + email: + on_success: change + on_failure: always diff --git a/README.adoc b/README.adoc index 660a0a2..1ccc213 100644 --- a/README.adoc +++ b/README.adoc @@ -1,3 +1,43 @@ = injarg -A command-line tool that allows to call an app with arguments from an args file. +---- +inject == inj[ect]args[s] +---- + +Command-line tool that allows to call an app with arguments from an args file. + +== Usage + +---- +injarg +---- + +== Installation + +Use just the following line to get the latest release. + +---- +luarocks install injarg +---- + +If you want to use the current development version, clone the repository and +use LuaRocks with the following command. + +---- +luarocks make dist/injarg-scn-0.rockspec +---- + +== Tests + +The unit tests are working with https://github.com/sstephenson/bats[bats]. +Use the folling lines to run the unit tests. + +---- +$ cd test +$ bats . +---- + +== License + +injarg is licensed under the MIT Open Source license. +For more information, see the LICENSE file in this repository. diff --git a/bin/injarg b/bin/injarg new file mode 100755 index 0000000..de5bf78 --- /dev/null +++ b/bin/injarg @@ -0,0 +1,360 @@ +#!/usr/bin/env lua + +-------------------------------------------------------------------------------- +--LuaZDF-begin --with argsfileinargs argsfilesindir insertall readargsfile shelljoin +-------------------------------------------------------------------------------- +local lfs = require( "lfs" ) --ZREQ-lfs +--ZFUNC-argsfileinargs-v1 +local function argsfileinargs( args ) --> filepath, idx, rest + local idx = nil + for k, v in pairs( args ) do + if v == "--args" then + idx = k + end + end + if not idx then return nil, idx, args end + -- we have a args parameter that should be used + local filepath = nil + local rest = {} + for i = 1, #args do + if i == idx then + -- nothing happens --args + elseif i == idx+1 then + filepath = args[ i ] + else + table.insert( rest, args[ i ] ) + end + end + return filepath, idx, rest +end +--ZFUNC-argsfilesindir-v1 +local function argsfilesindir( appname, dir ) --> defargs, filepaths + --ZFUNC-dirfiles-v1 + local function dirfiles( path ) --> iter + local function yielddir( path ) + for entry in lfs.dir( path ) do + local entrypath = path.."/"..entry + local mode = lfs.attributes( entrypath, "mode" ) + if mode == "file" then + coroutine.yield( entry ) + end + end + end + return coroutine.wrap( function() yielddir( path ) end ) + end + --ZFUNC-endswith-v1 + local function endswith( str, suffix ) --> res + return string.sub( str, -string.len( suffix ) ) == suffix + end + --ZFUNC-startswith-v1 + local function startswith( str, prefix ) --> res + return string.sub( str, 1, string.len( prefix ) ) == prefix + end + dir = dir or "." + local defargs = nil + local filepaths = {} + -- check for .args files in the working directory + for filename in dirfiles( dir ) do + if filename == appname..".auto.args" then + defargs = filename + elseif startswith( filename, appname ) and + endswith( filename, ".args" ) then + table.insert( filepaths, filename ) + end + end + if #filepaths > 0 then + return defargs, filepaths + end + + return defargs, nil +end +--ZFUNC-insertall-v1 +local function insertall( arr, pos, oth ) --> arr + if not oth then + oth = pos + pos = #oth + 1 + end + for _, v in ipairs( oth ) do + table.insert( arr, pos, v ) + pos = pos + 1 + end + return arr +end +--ZFUNC-readargsfile-v1 +local function readargsfile( filepath ) --> args, err + --ZFUNC-appendall-v1 + local function appendall( arr, oth ) --> arr + for _, v in ipairs( oth ) do + table.insert( arr, v ) + end + return arr + end + --ZFUNC-readlines-v1 + local function readlines( filepath ) + local f, err = io.open( filepath, "r" ) + if err then return nil, err end + local strlst = {} + for line in f:lines() do + table.insert( strlst, line ) + end + local res, err = f:close() + if err then return nil, err end + return strlst + end + --ZFUNC-rmprefix-v1 + local function rmprefix( str, prefix ) + local prefixlen = string.len( prefix ) + local startsub = string.sub( str, 1, prefixlen ) + if startsub == prefix then + return string.sub( str, prefixlen + 1 ) + else + return str + end + end + --ZFUNC-shellsplit-v1 + local function shellsplit( line ) --> args + --ZFUNC-trim-v1 + local function trim( str ) + local n = str:find( "%S" ) + return n and str:match( ".*%S" ) or "" + end + --ZFUNC-utf8codes-v1 + local function utf8codes( str ) + return str:gmatch( "[%z\1-\127\194-\244][\128-\191]*" ) + end + local function isspace( str ) + return str == " " or str == "\t" or str == "\r" or str == "\n" + end + line = trim( line ) + local args = {} + local buff = {} + local escaped, doubleQ, singleQ, backQ = false, false, false, false + for r in utf8codes( line ) do + if escaped then----------------------------------------------------------- + table.insert( buff, r ) + escaped = false + elseif r == '\\' then----------------------------------------------------- + if singleQ then + table.insert( buff, r ) + else + escaped = true + end + elseif isspace( r ) then-------------------------------------------------- + if singleQ or doubleQ or backQ then + table.insert( buff, r ) + else + table.insert( args, table.concat( buff ) ) + buff = {} + end + elseif r == "`" then------------------------------------------------------ + if singleQ or doubleQ then + table.insert( buff, r ) + else + backQ = not backQ + end + elseif r == '"' then------------------------------------------------------ + if singleQ or backQ then + table.insert( buff, r ) + else + doubleQ = not doubleQ + end + elseif r == "'" then------------------------------------------------------ + if doubleQ or backQ then + table.insert( buff, r ) + else + singleQ = not singleQ + end + else---------------------------------------------------------------------- + table.insert( buff, r ) + end + end + if #buff > 0 then table.insert( args, table.concat( buff ) ) end + return args + end + local function appendtolast( tab, i, j ) + local val = tab[ #tab ] or "" + val = val..i..j + tab[ #tab ] = val + return tab + end + local lines, err = readlines( filepath ) + if err then return nil, err end + local args = {} + for i, line in ipairs( lines ) do + if #line == 0 then + --we ignore empty lines + elseif line:match( "^#" ) then + --we ignore comments + elseif line:match( "^$ " ) then + appendall( args, shellsplit( rmprefix( line, "$ " ) ) ) + elseif line:match( "^| " ) then + appendtolast( args, "", rmprefix( line, "| " ) ) + elseif line:match( "^|= " ) then + appendtolast( args, "=", rmprefix( line, "|= " ) ) + elseif line:match( "^|s " ) then + appendtolast( args, " ", rmprefix( line, "|s " ) ) + elseif line:match( "^|t " ) then + appendtolast( args, "\t", rmprefix( line, "|t " ) ) + elseif line:match( "^|n " ) then + appendtolast( args, "\n", rmprefix( line, "|n " ) ) + else + table.insert( args, line ) + end + end + return args +end +--ZFUNC-shelljoin-v1 +local function shelljoin( args ) --> line + --ZFUNC-escapeshellarg-v1 + local function escapeshellarg( str ) + return '"'..str:gsub( '"', '\\"' )..'"' + end + --ZFUNC-isoneshellarg-v1 + local function isoneshellarg( str ) + --ZFUNC-utf8codes-v1 + local function utf8codes( str ) + return str:gmatch( "[%z\1-\127\194-\244][\128-\191]*" ) + end + local function isspace( str ) + return str == " " or str == "\t" or str == "\r" or str == "\n" + end + local inbuff = false + local escaped, doubleQ, singleQ, backQ = false, false, false, false + for r in utf8codes( str ) do + if escaped then-------------------------------------------------------- + inbuff = true + escaped = false + elseif r == '\\' then-------------------------------------------------- + if singleQ then inbuff = true + else escaped = true + end + elseif isspace( r ) then----------------------------------------------- + if singleQ or doubleQ or backQ then inbuff = true + else return false + end + elseif r == "`" then--------------------------------------------------- + if singleQ or doubleQ then inbuff = true + else backQ = not backQ + end + elseif r == '"' then--------------------------------------------------- + if singleQ or backQ then inbuff = true + else doubleQ = not doubleQ + end + elseif r == "'" then--------------------------------------------------- + if doubleQ or backQ then inbuff = true + else singleQ = not singleQ + end + else------------------------------------------------------------------- + inbuff = true + end + end + if escaped or doubleQ or singleQ or backQ then return false end + if inbuff then return true end + return false --no argument + end + local tmp = {} + for _, a in ipairs( args ) do + if not isoneshellarg( a ) then + a = escapeshellarg( a ) + end + table.insert( tmp, a ) + end + return table.concat( tmp, " " ) +end +-------------------------------------------------------------------------------- +--LuaZDF-end +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- +-- Output Util Functions +-------------------------------------------------------------------------------- + +local debug = false +local normalOutput = true + +local function errexit() + os.exit( 1 ) +end + +local function println( ... ) + if normalOutput then + print( ... ) + end +end + +local function printfln( str, ... ) + if normalOutput then + return io.stdout:write( str:format( ... ), "\n" ) + end +end + +local function errfln( str, ... ) + return io.stderr:write( "Error: ", str:format( ... ), "\n" ) +end + +-------------------------------------------------------------------------------- + +local function usage() + return "injarg [ ... --args ... ]" +end + +local function handleargsfile( filepath ) + local args, err = readargsfile( filepath ) + if err then + errfln( "Not able to open args-file %q", filepath ) + errfln( err ) + errexit() + end + + return args +end + +-------------------------------------------------------------------------------- +-- +-------------------------------------------------------------------------------- +if #arg < 1 then + errfln( "Need at least the app name." ) + printfln( usage() ) + errexit() +end + +local appname = table.remove( arg, 1 ) + +local defargs, filepaths = argsfilesindir( appname ) + +----------------------------------------------------------------------- app mode +if #arg == 0 then + if not defargs and not filepaths then + errfln( "No default args file detected for %q", appname ) + errexit() + end + + if not filepaths then filepaths = {} else table.sort( filepaths ) end + if defargs then table.insert( filepaths, 1, defargs ) end + + if #filepaths > 1 then + errfln( "Please select one of this args files:" ) + for _, path in pairs( filepaths ) do + println( path ) + end + errexit() + end + + local args = handleargsfile( filepaths[ 1 ] ) + table.insert( args, 1, appname ) + println( shelljoin( args ) ) + +---------------------------------------------------------------------- args mode +else + local filepath, idx, rest = argsfileinargs( arg ) + if not filepath then + errfln( "no --args parameter detected in: %s", table.concat( arg, " " ) ) + errexit() + end + + local args, err = handleargsfile( filepath ) + insertall( rest, idx, args ) + table.insert( rest, 1, appname ) + println( shelljoin( rest ) ) + +end diff --git a/test/ffmpeg1/config.args b/test/ffmpeg1/config.args new file mode 100644 index 0000000..a23d3be --- /dev/null +++ b/test/ffmpeg1/config.args @@ -0,0 +1,12 @@ + +-strict +experimental + +-deinterlace + +-vcodec +h264 + +-acodec +aac + diff --git a/test/ffmpeg1/ffmpeg.auto.args b/test/ffmpeg1/ffmpeg.auto.args new file mode 100644 index 0000000..f5bda71 --- /dev/null +++ b/test/ffmpeg1/ffmpeg.auto.args @@ -0,0 +1,17 @@ +-nostdin + +-b +250k + +-strict +experimental + +-deinterlace + +-vcodec +h264 + +-acodec +aac + +example.mp4 diff --git a/test/ffmpeg2/ffmpeg.altname.args b/test/ffmpeg2/ffmpeg.altname.args new file mode 100644 index 0000000..64e7d32 --- /dev/null +++ b/test/ffmpeg2/ffmpeg.altname.args @@ -0,0 +1,7 @@ +-vcodec +h264 + +-acodec +aac + +long alternative name.mp4 diff --git a/test/ffmpeg2/ffmpeg.args b/test/ffmpeg2/ffmpeg.args new file mode 100644 index 0000000..0d8cb34 --- /dev/null +++ b/test/ffmpeg2/ffmpeg.args @@ -0,0 +1,7 @@ +-vcodec +h264 + +-acodec +aac + +example.mp4 diff --git a/test/ffmpeg2/ffmpeg.shortname.args b/test/ffmpeg2/ffmpeg.shortname.args new file mode 100644 index 0000000..50a024f --- /dev/null +++ b/test/ffmpeg2/ffmpeg.shortname.args @@ -0,0 +1,7 @@ +-vcodec +h264 + +-acodec +aac + +e.mp4 diff --git a/test/ffmpeg2/strange.args b/test/ffmpeg2/strange.args new file mode 100644 index 0000000..c346d23 --- /dev/null +++ b/test/ffmpeg2/strange.args @@ -0,0 +1,2 @@ +#just a name +long name.mp4 diff --git a/test/test.bats b/test/test.bats new file mode 100644 index 0000000..1f89d03 --- /dev/null +++ b/test/test.bats @@ -0,0 +1,71 @@ +#!/usr/bin/env bats + +#------------------------------------------------------------------------ errors + +@test "start with no app name" { + run ../bin/injarg + [ "$status" -eq 1 ] + [ "${lines[0]}" = 'Error: Need at least the app name.' ] + [ "${lines[1]}" = 'injarg [ ... --args ... ]' ] +} + +@test "start with no " { + +} + +#----------------------------------------------------------------------- ffmpeg1 + +@test "ffmpeg1: auto load default args file" { + cd ffmpeg1 + run ../../bin/injarg ffmpeg + [ "$status" -eq 0 ] + [ "$output" = 'ffmpeg -nostdin -b 250k -strict experimental -deinterlace -vcodec h264 -acodec aac example.mp4' ] +} + +@test "ffmpeg1: select via --args" { + cd ffmpeg1 + run ../../bin/injarg ffmpeg --args ffmpeg.auto.args + [ "$status" -eq 0 ] + [ "$output" = 'ffmpeg -nostdin -b 250k -strict experimental -deinterlace -vcodec h264 -acodec aac example.mp4' ] +} + +@test "ffmpeg1: inject values" { + cd ffmpeg1 + run ../../bin/injarg ffmpeg -nostdin -b 250k --args config.args example.mp4 + [ "$status" -eq 0 ] + [ "$output" = 'ffmpeg -nostdin -b 250k -strict experimental -deinterlace -vcodec h264 -acodec aac example.mp4' ] +} + +#----------------------------------------------------------------------- ffmpeg2 + +@test "ffmpeg2: try to load default args file" { + cd ffmpeg2 + run ../../bin/injarg ffmpeg + [ "$status" -eq 1 ] + [ "${lines[0]}" = 'Error: Please select one of this args files:' ] + [ "${lines[1]}" = 'ffmpeg.altname.args' ] + [ "${lines[2]}" = 'ffmpeg.args' ] + [ "${lines[3]}" = 'ffmpeg.shortname.args' ] +} + +@test "ffmpeg2: select via --args file" { + cd ffmpeg2 + run ../../bin/injarg ffmpeg --args ffmpeg.args + [ "$status" -eq 0 ] + [ "$output" = 'ffmpeg -vcodec h264 -acodec aac example.mp4' ] +} + +@test "ffmpeg2: select via --args altname file" { + cd ffmpeg2 + run ../../bin/injarg ffmpeg --args ffmpeg.altname.args + [ "$status" -eq 0 ] + [ "$output" = 'ffmpeg -vcodec h264 -acodec aac "long alternative name.mp4"' ] +} + +@test "ffmpeg2: select via --args shortname file" { + cd ffmpeg2 + run ../../bin/injarg ffmpeg --args ffmpeg.shortname.args + [ "$status" -eq 0 ] + [ "$output" = 'ffmpeg -vcodec h264 -acodec aac e.mp4' ] +} +