-- $Id: shellExtension.lua 105148 2010-05-06 14:10:25Z ururahy $

-------------------------------------------------------------------------------
-- Biblioteca de extensoes do Shell para executar algoritmos legados no CSBase.
--
-- ATENCAO: Todos os scripts .lua dos algoritmos dos sistemas (websintesi,
-- infogrid, csgrid, marlim) usam este arquivo. Alteracoes devem ser
-- comunicadas aos responsaveis dos projetos para que testes sejam feitos.
--
-- Autores: Leandro Pinto (lmoreira)
--          Taciana Melcop (taciana)
--          André Costa (costa)
--          Cristina Ururahy
--			Isabella Almeida
-------------------------------------------------------------------------------

local assert       = assert
local ipairs       = ipairs
local tostring     = tostring
local tonumber     = tonumber
local pairs        = pairs
local require      = require
local print        = print
local coroutine    = coroutine
local type         = type

local io     = require "io"
local os     = require "os"
local table  = require "table"
local string = require "string"
local math   = require "math"

module "shellExtension"

-- variáveis globais
FILESEPARATOR_WIN = "\\"
FILESEPARATOR_UNIX = "/"
LINEBREAK_WIN = string.char(13,10) -- CR LF
SHELL_BASH = "bash"
SHELL_SH = "sh"
SHELL_KSH = "ksh"

fileSeparator = fileSeparator or FILESEPARATOR_UNIX

local _availableShells = { [SHELL_BASH] = true, [SHELL_SH] = true, 
  [SHELL_KSH] = true, }

-- ------------------------------------------------------------
-- Obtem uma tabela contendo todos os parâmetros da linha de comando.
--
-- Parametros:
-- command - A linha de comando.
-- name - O nome do parâmetro.
--
-- Valores de retorno:
-- O valor do parametro. Se nao for encontrado, retornara nil.
-- Uma explicação textual sobre o erro ou nil (sucesso).

local _parameterCache = {}

function getParameterValue (command, name)
  local value = nil
  local errorMessage = nil

  -- ainda não existia entrada na cache para 'command'
  if not _parameterCache[command] then
    _parameterCache[command] = {}
    for _, param in pairs(command) do
      string.gsub(param, "(.-)=(.*)", function (name, val)
        _parameterCache[command][name] = val
      end)
    end
  end

  -- se já processamos este nome antes, retorna direto da cache
  if not _parameterCache[command][name] then
    errorMessage = "parametro "..name.." nao foi encontrado"
  end

  return _parameterCache[command][name], errorMessage
end

-- ============================================================
-- FUNÇÕES PARA MANIPULAÇÃO DE TABELAS

-- retorna uma nova tabela que é uma "shallow copy" de outra (i.e.
-- campos que sejam tabelas serão copiados por referência)
local function _tableCopy (from)
  local to = {}
  for k, v in pairs(from) do
    to[k] = v
  end
  return to
end

-- ============================================================
-- FUNÇÕES PARA MANIPULAÇÃO DE STRINGS

local _CMD_DISCARD = " > /dev/null 2>&1"
local _CMD_ERR_DISCARD = " 2> /dev/null"

-- na ausência do suporte à API POSIX, chdir() deve ser "simulado" a
-- cada chamada; para isto, implementamos o conceito de diretório
-- corrente através de _CURRENT_DIR
local _CURRENT_DIR = "."..fileSeparator

-- iterador: retorna todas as linhas de um texto, uma de cada vez
function strLineIterator (text)
  -- garante que 'text' termine com \n, a menos que a string seja vazia.
  if string.match(text,"%S") and string.sub(text, -1, -1) ~= "\n" then
    text = text.."\n"
  end
  return coroutine.wrap(function ()
    for line in string.gmatch(text, "(.-)\n") do
      coroutine.yield(line)
    end
  end)
end


-- ------------------------------------------------------------
-- Cria um nome de um arquivo temporário
-- NOTA: Essa função foi criada para substituir o uso de 
-- os.tmpname() em função da issue CSBASE-3973.

function tmpname()
  local name = os.tmpname( )
  while name:sub(#name) == '.' do
    name = os.tmpname( )
  end
  return name
end

-- ------------------------------------------------------------
-- retorna uma cópia de 'str' aonde as ocorrências de patt
-- foram trocadas por repl

function strReplaceAll (str, patt, repl)
  str, nchanges = string.gsub(str, patt, repl)
  while nchanges and nchanges > 0 do
    str, nchanges = string.gsub(str, patt, repl)
  end
  return str
end

-- ------------------------------------------------------------
-- retorna uma cópia de 'str' aonde as ocorrências do char 'dupl' foram
-- removidas

function strRemoveDuplicates (str, dupl)
  return string.gsub(str, dupl.."+", dupl)
end

-- ------------------------------------------------------------
-- retorna a primeira linha de um texto

function strGetFirstLine (text)
  local i = string.find(text, "\n")
  return i and string.sub(text, 1, i-1) or text
end

-- ------------------------------------------------------------
-- retorna uma string que pode ser usada para comparações independente
-- de maiúsculas/minúsculas. p.ex.: abcdef --> [aA][bB][cC][dD][eE][fF]

function strCaseEscape (str)
  -- usamos um par extra de parênteses para forçar apenas um valor de
  -- retorno
  return (string.gsub(str, "%a", function (c)
    return "["..string.lower(c)..string.upper(c).."]"
  end))
end

-- ------------------------------------------------------------
-- faz o escape de alguns dos caracteres especiais para regexps (p.ex.
-- "arq-saida.dat" --> "arq%-saida%.dat")

function regexpEscape (regexp, caseSensitive)
  if not caseSensitive then
	regexp = strCaseEscape(regexp)
  end
  return string.gsub(regexp, "([-.%%*])", "%%%1")
end

-- ------------------------------------------------------------

function quoteEscape (str)
  return string.gsub(str, "\\", "\\\\")
end

-- ------------------------------------------------------------
-- substitui 'mark' em um path pelo separador de diretórios em uso
-- FIXME a rigor deveríamos usar _regexpEscape(mark)...

local function _fixFileSep (path, mark, newSep)
  return string.gsub(path, mark, newSep or fileSeparator)
end

-- ============================================================
-- FUNÇÕES PARA MANIPULAÇÃO DE ARQUIVOS E DIRETÓRIOS

-- ------------------------------------------------------------
-- Separa um path na forma /aaa/bbb/ccc/file em dir + arquivo (no caso,
-- dir = /aaa/bbb/ccc/ e arquivo = file)
--
-- Parametros:
--   path : O caminho completo (incluindo o nome do arquivo)
--
-- OBS.: o último componente do path é _sempre_ separado, independente
-- do path terminar com '/'. P.ex.: /aaa/bbb/ccc/ --> /aaa/bbb/ , ccc/
--
-- Valores de retorno:
--   diretorio
--   arquivo
--   mensagem de erro ou nil (sucesso)

function splitPath (path)
  local dir = ""
  local file = path
  local patt
  patt = _fixFileSep("^(.*/)(.+)$", "/")
  string.gsub(path, patt, function (p, f)
    dir, file = p, f
  end)
  return dir, file
end

-- ------------------------------------------------------------
-- Transforma uma parametro do tipo array de strings (String concatenada
-- com ',' as separando) para um array
--
-- Parametros:
-- valor O valor do parametro.
--
-- Valores de retorno:
-- A tabela com as strings.
--
-- OBS.: o primeiro índice da tabela de resultados é UM

function toStringArray (valueList)
  local tableValue = {}

  for val in string.gmatch(valueList, "(.-),") do
    table.insert(tableValue, val)
  end

  -- temos que cobrir o caso em que a string não termina com ","
  string.gsub(valueList, "([^,]*[^,])$", function (last)
    table.insert(tableValue, last)
  end)

  return tableValue
end

-- ------------------------------------------------------------
-- Transforma uma parametro do tipo array de strings (String concatenada
-- com ',' as separando) para uma string com campos separados por \n
-- Assumimos que a string **não** termina com ","
--
-- Parametros:
-- str O valor do parametro.
-- newSeparator (opcional) outro valor para o "\n"
-- oldSeparator (opcional) outro valor para a ","
--
-- Valores de retorno:
-- uma string com campos separados por \n,
-- o número de campos que há na string.

function splitWithLineFeed (str, newSeparator, oldSeparator)
  oldSeparator = oldSeparator or ","
  newSeparator = newSeparator or "\n"
  local fields, num_fields = string.gsub(str,"(.-)"..oldSeparator,
    "%1"..newSeparator)
  num_fields = num_fields + 1
  return fields, num_fields
end

-- ------------------------------------------------------------
-- retorna 'path' prefixado por _CURRENT_DIR; as exceções são para os
-- casos em que 'path' é absoluto ou começa com '~' -- neste caso, o
-- próprio path é retornado

local function _currDir (path)
  -- se path é absoluto, não altera
  if string.find(path, string.format("^[~%q].*", fileSeparator)) then
    return path
  else
    return shrinkDir(_CURRENT_DIR..fileSeparator..path)
  end
end

-- ------------------------------------------------------------
-- prefixa um comando com o chdir necessário

local function _chdir (cmd)
  return "cd '".._CURRENT_DIR.."' && "..cmd
end

-- ------------------------------------------------------------
-- retorna true se a string for um parâmetro válido, isto é, 
-- não for composta por nenhuma das sequencias de caracteres abaixo:
--  ;  |   &    &&
-- Retorna false caso contrário.
local function isValidParam (str)
	if not str then return true end
	local result = string.match(str, "[|;&]") or string.match(str, "&&")
	return result == nil
end

-- ------------------------------------------------------------
-- executa um comando, antes indo para _CURRENT_DIR
-- RETORNA: true = sucesso, false = erro

local function _executeShell (shell, cmd)
   local shell_cmd = string.format("%s -c \"%s\"", shell, _chdir(cmd))
   return (os.execute(shell_cmd) == 0)
end

local function _execute (cmd)
  return _executeShell(SHELL_BASH, cmd)
end

-- ------------------------------------------------------------
-- retorna uma tabela com as seguintes informações de um dado path:
-- {['f'] = true ou false se o path for um arquivo
--  ['d'] = true ou false se o path for um diretório
--  ['h'] = true ou false se o path for um link simbólico
--  ['r'] = true ou false se for possível ler o arquivo
--  ['w'] = true ou false se for possível escrever no arquivo
--  ['x'] = true ou false se for possível executar o arquivo
-- }
function getAttributes(path)
  local tmpname = tmpname()

  local f = "test -f '%s' && echo -n f;"
  local d = "test -d '%s' && echo -n d;"
  local h = "test -h '%s' && echo -n h;"
  local r = "test -r '%s' && echo -n r;"
  local w = "test -w '%s' && echo -n w;"
  local x = "test -x '%s' && echo -n x"

  local cmd = "("..f..d..h..r..w..x..") > %s"

  cmd = string.format(cmd, path, path, path, path, path, path, tmpname)
  
  _execute(cmd)

  local file = fileOpen(tmpname, "r")
  if not file then
    local msg = "Falha na abertura do arquivo temporário. [%s] de saída do comando:%s."
    return nil, string.format(mgs, tmpname, cmd)
  end
  local content = file:read("*a")
  file:close()
  os.remove(_currDir(tmpname))
 
  local result = {}
  result.f = string.find(content, 'f') ~= nil
  result.d = string.find(content, 'd') ~= nil
  result.h = string.find(content, 'h') ~= nil
  result.r = string.find(content, 'r') ~= nil
  result.w = string.find(content, 'w') ~= nil
  result.x = string.find(content, 'x') ~= nil
  return result
end

-- ------------------------------------------------------------
-- retorna true se 'file' existe
-- RETORNA: true = sucesso, false = erro

function fileExists (file)
  return _execute("test -f '"..file.."'")
end

-- ------------------------------------------------------------
-- retorna true se 'dir' é um diretório

function isDir (dir)
  return _execute("test -d '"..dir.."'")
end

-- ------------------------------------------------------------
-- retorna true se temos permissão para leitura do dado path
function canRead(path)
  return _execute("test -r '"..path.."'")
end

-- ------------------------------------------------------------
-- retorna true se temos permissão para escrita do dado path
function canWrite(path)
  return _execute("test -w '"..path.."'")
end

-- ------------------------------------------------------------
-- retorna true se temos permissão para execução do dado path
function canExecute(path)
  return _execute("test -x '"..path.."'")
end

-- ------------------------------------------------------------
-- retorna true se o dado path aponta para um link simbólico
function isSymbolicLink(path)
  return _execute("test -h '"..path.."'")
end

-- ------------------------------------------------------------
-- retorna uma tabela onde cada índice é uma linha da saida
-- do comando 'ls <arg> <path>'.

function ls(path, arg)
  local tmpname = tmpname()
	
  if not isValidParam(arg) then
    return nil, "Argumento possui caracteres inválidos."	
  end

  if not arg then arg = '' end

  local lscommand = string.format("ls %s %s > %s", arg, path, tmpname)

  _execute(lscommand)
  local file = fileOpen(tmpname, "r")
  if not file then
    return nil, "Falha na abertura do arquivo temporário. ["..tmpname.."] de saída do comando: '"..lscommand.."'."
  end
    
  local result = {}
  for line in file:lines() do
    result[#result + 1] = line      
  end
  file:close()
  os.remove(_currDir(tmpname))

  return result
end

-- ------------------------------------------------------------
-- retorna o tamanho de um diretório ou arquivo dado seu path.
-- retorna nil em caso de erro ou path inexistente.

function size(path)
  if isDir(path) then return 0 end

  local file, err = io.open(path, "r")
  if err then
    return -1
  end
  
  local s = file:seek("end")
  file:close()
  return s
end

-- ------------------------------------------------------------
-- retorna true se 'dir' está vazio

function isEmptyDir (dir)
  local isDir = isDir(dir)
  local cmd = "test `ls -a1 '"..dir.."' | head -3 | wc -l` -lt 3"
  return isDir and _execute(cmd)
end

-- ------------------------------------------------------------
-- cria um diretório
-- RETORNA: true = sucesso, false = erro

function mkdir (dir)
   return _execute("mkdir '"..dir.."'".._CMD_DISCARD)
end

-- ------------------------------------------------------------
-- cria um diretório e todos os subdiretórios intermediários, se não existirem
-- RETORNA: true = sucesso, false = erro

function mkdirs (dir)
   return _execute("mkdir -p '"..dir.."'".._CMD_DISCARD)
end

-- ------------------------------------------------------------
-- remove um diretório
-- RETORNA: true = sucesso, false = erro

function rmdir (dir)
  if isDir(dir) then
    return _execute("rm -rf '"..dir.."'".._CMD_DISCARD)
  end
  return nil
end

-- ------------------------------------------------------------
-- Em alguns casos a string que representa o diretório corrente
-- começou a ficar grande demais!
-- Troca os casos xx/yy/../ para xx/ e os casos // e /./ para /
-- xx/. para xx

function shrinkDir (dir)
  dir = strReplaceAll (dir, "([^/%.]+/%.%./)", "")
  dir = strReplaceAll (dir, "(/%.?/)", "/")
  dir = strReplaceAll (dir, "([^/%.]+)/%.$", "%1")
  return dir
end

-- ------------------------------------------------------------
-- altera o diretório corrente. se o novo diretório foi especificado com
-- path absoluto, este passa a ser o diretório corrente; senão, o novo
-- diretório é anexado ao fim do diretório corrente

function chdir (dir)
  if string.find(dir, string.format("^[~%q].*", fileSeparator)) then
    _CURRENT_DIR = dir
  else
    _CURRENT_DIR = shrinkDir (_CURRENT_DIR..fileSeparator..dir)
  end
  return _CURRENT_DIR
end

-- ------------------------------------------------------------
-- retorna o valor associado a uma variável de ambiente 
-- Não pode usar getenv de Lua pois não pega $$

function getenv (var)
  local result = io.popen("echo $"..var)
  local env = result:read()
  result:close()
  return env
end

-- ------------------------------------------------------------
-- copia arquivo 'from' para 'to'
-- RETORNA: true = sucesso, false = erro

function fileCopy (from, to)
  return _execute("cp '"..from.."' '"..to.."'".._CMD_DISCARD)
end

-- ------------------------------------------------------------
-- move arquivo 'from' para 'to'
-- RETORNA: true = sucesso, false = erro

function fileMove (from, to)
  -- OBS.: não usamos os.rename() porque queremos tirar proveito dos
  -- wildcards (*, ? etc.) do shell
  return _execute("mv -f '"..from.."' '"..to.."'".._CMD_DISCARD)
end

-- ------------------------------------------------------------
-- acrescenta texto ao final de um arquivo

function fileAppend (fileName, text)
  local fHandle, err = fileOpen(fileName, "a+")
  assert(fHandle, err)
  fHandle:write(text)
  fHandle:close()
end

-- ------------------------------------------------------------
-- remove arquivo
-- RETORNA: true = sucesso, false = erro

function fileRemove (name)
--  return (os.remove(_currDir(name)) ~= nil)
  return _execute("rm -f '"..name.."'".._CMD_DISCARD)
end

-- ------------------------------------------------------------
-- abre um arquivo no modo especificado.
-- RETORNA: filehandle (sucesso) ou nil, erro (erro)

function fileOpen (name, mode)
  return io.open(_currDir(name), mode)
end

-- ------------------------------------------------------------
-- retorna:
--     o primeiro componente de um path
--     o resto do path (excluindo o separador)
-- Exemplos:
-- /aaa/bbb --> /aaa
-- aaa      --> aaa
-- aaa/bbb  --> aaa
-- /        --> /

local function _getFirstDirPart (path)
  -- não estamos interessados em lidar com repetições de '/'
  path = strRemoveDuplicates(path, fileSeparator)
  local repl = _fixFileSep("^(/?[^/].-)/.*", "/")
  p = string.gsub(path, repl, "%1", 1)
  local len = string.len(p)
  -- len+2 para não considerarmos o separador
  return p, string.sub(path, len+2)
end

-- ------------------------------------------------------------
-- retorna um path absoluto associado a um diretório
-- ASSERTIVA: 'relPath' é um path terminado em '/'

function dirGetAbsolutePath (relPath)
  return dirGetAbsolutePathShell(SHELL_BASH, relPath)
end

function dirGetAbsolutePathShell (shell, relPath)
  assert(_availableShells[shell], "shell "..shell.." nao suportado")

  if not isDir(relPath) then
	return nil
  end
  local absPathCmd = shell.." -c \"cd '"..relPath.."' && pwd\""
  local absPathHandle = io.popen(absPathCmd)
  local result = absPathHandle:read("*l")
  if not result then
  	return nil
  end
  local absPath = string.gsub(result, "%s+$", "")
  absPathHandle:close()
  return absPath
end

-- ------------------------------------------------------------
-- retorna um path absoluto associado a um arquivo
-- ASSERTIVA: 'relPath' é um path para um ARQUIVO

function fileGetAbsolutePath (relPath)
  local dir, file = splitPath(relPath)
  local absoluteDirPath = dirGetAbsolutePath(dir)
  if not absoluteDirPath then
    return nil
  end
  return absoluteDirPath..fileSeparator..file
end

-- ------------------------------------------------------------
-- Retorna lista dos arquivos/diretórios no dir corrente, um nome por linha, que
-- casam com o padrão fornecido. Retorna uma string vazia quando nenhum arquivo
-- casar com o padrão.
-- Se o parâmetro onlyOne for true, retorna apenas um arquivo/diretório.
-- ATENÇÃO: não lista arquivos cujos nomes começam com "." a não ser que o pattern comece por "." 

-- NÃO É RECURSIVO
-- FALTA LISTA DE PATTERNS
local function listChildren(patternParam, caseSensitiveParam, excludeParam, onlyDirParam, onlyOneParam)

  local dir, patternTable
  if not patternParam or patternParam  == "" then
	patternTable = {}
  elseif type(patternParam) ~= type({}) then
  	local patt
    dir, patt = splitPath(patternParam or "")
    patternTable = { patt }
  else
	patternTable = patternParam
  end
 
  if not dir or dir == "" then
  	dir = "."
  end
  
  local patternLine = ""
  local operator = ""
  for i, patt in ipairs(patternTable) do
  	if patt ~= "" then
		if splitPath(patt) ~= "" then
			return nil,  "Erro: tabela de patterns não pode conter diretório!"
		end
		if not caseSensitiveParam then
	    	patt = strCaseEscape(patt)
	    end
		
		local exclude = ""
	  	if excludeParam then	
		  exclude = "\\\!"
	  	end
	
	    --
	    -- Verifica se o pattern começa com '.'.
	    --
	    -- No Solaris, o find não retorna arquivos que começam com '.'. Para que as
	    -- demais plataformas fiquem compatíveis, retiramos explicitamente da lista os
	    -- arquivos que começam com '.'. Há uma exceção quando o pattern contém o '.'.
	    local removeDot = ""
	    if string.sub(patt, 1, 1) ~= '.' or excludeParam then
	      removeDot = "-a -name '[\!.]*'"
	    end
		patternStr = "%s \\\( %s -name '%s' %s \\\) "
		patternLine = patternLine..string.format(patternStr, operator, exclude, patt, removeDot)
		if excludeParam then
		  operator = "-a"
		else
		  operator = "-o"
		end
  	end
  end

  if patternLine == "" then
	patternLine = "-name '[\!.]*'"
  end

  local onlyDir = ""
  if onlyDirParam then
    onlyDir = "-type d"
  end

  local onlyOne = ""
  if onlyOneParam then
    onlyOne = "| head -1"
  end

  -- Retira a última barra do diretório
  dir = string.gsub(dir, "(.+)\/$","%1")
  local _, dirname = splitPath(dir)

  local removeDotDir = ""
  if dir == "." then
    removeDotDir = "| cut -c3- "
  end

  local findStr = " ( /usr/bin/find %s %s \\\! -name '%s' -prune -a \\\( %s \\\) -print %s %s) "
  local findCmd = string.format(findStr, dir, onlyDir, dirname, patternLine, removeDotDir, onlyOne).._CMD_ERR_DISCARD
--  print("FIND", _chdir(findCmd))
  local result = io.popen(_chdir(findCmd))
  local files = result:read("*a")
  result:close()
  return files
end

-- ------------------------------------------------------------
-- Retorna apenas um arquivo/diretório no dir corrente, que case com o padrão
-- fornecido. Retorna uma string vazia se nenhum arquivo casar com o padrão.
-- ATENÇÃO: não lista arquivos cujos nomes começam com "." 

function listOneFile (pattern, caseSensitive, exclude)
  local files, err = listChildren(pattern, caseSensitive, exclude, false, true)
  if files then
  	return (string.gsub(files, "(%w-)\n.*","%1")) -- Apenas o primeiro, sem o LF.
  else 
	return nil, err
  end
end

function listFiles (pattern, caseSensitive, exclude)
  return listChildren(pattern, caseSensitive, exclude, false, false)
end

-- ------------------------------------------------------------
-- retorna lista dos diretórios no dir corrente, um nome por
-- linha. Exclui os diretórios que casam com o parâmetro "excludePattern".
-- Este parâmetro é case insensitive.
-- ATENÇÃO: não lista diretórios cujos nomes começam com "." 

function listDirs (pattern, caseSensitive, exclude)
  return listChildren(pattern, caseSensitive, exclude, true, false)
end

-- ------------------------------------------------------------
-- cria arquivo com tamanho 0 (ATENÇÃO: destrói arquivos já existentes)

function emptyFile (name)
  local fileHandle = io.open(_currDir(name), "w")
  fileHandle:close()
end

-- ------------------------------------------------------------
-- Altera o título da janela do shell que está executando o comando

function setWinTitle(title)
  if title and tostring(title) then
    print("\027];"..title.."\007")
  end
end

-- ------------------------------------------------------------
-- grava fileOut como cópia de fileIn aonde todas as ocorrências de
-- "search" foram trocadas por "replace". "search" e "replace" sao fornecidos
-- aos pares na tabela

function fileReplaceTexts (fileIn, fileOut, tableArg)
  local fileOutHandle = io.open(_currDir(fileOut), "w")
  assert(fileOutHandle, "impossivel criar arquivo de saida "..fileOut)
  for line in io.lines(_currDir(fileIn)) do
    local newLine, count
    for i=1, #tableArg, 2 do
      newLine, count = string.gsub(line, tableArg[i], tableArg[i+1])
      if count > 0 then break end
    end
    fileOutHandle:write((newLine or line).."\n")
  end
  fileOutHandle:close()
end

-- ------------------------------------------------------------
-- grava fileOut como cópia de fileIn aonde todas as ocorrências de
-- "search" foram trocadas por "replace". 

function fileReplaceText(fileIn, fileOut, search, replace)
  local searchReplace = {}
  table.insert(searchReplace, search)
  table.insert(searchReplace, replace)
  fileReplaceTexts(fileIn, fileOut, searchReplace)
end


local function _makeRedirection(logFile, showOutput)
  local redirect = ""
  if logFile then
    if showOutput then
      redirect = " 2>&1 | tee '"..logFile.."'"
    else
      redirect = " > '"..logFile.."' 2>&1"
    end
  else 
    if not showOutput then
      redirect = " > /dev/null 2>&1"
    end
  end
  return redirect
end

-- ------------------------------------------------------------
-- executa comando 'cmd', opcionalmente direcionando saída para um
-- logFile
-- showOutput determina se a saída deve ser mostrada
-- RETORNA: true = sucesso, false = erro

function execCmd (shell, cmd, logFile, showOutput)
  assert(_availableShells[shell], "shell "..shell.." nao suportado")

  local redirect = _makeRedirection(logFile, showOutput)
  -- não precisamos diferenciar bash de sh no que diz respeito à
  -- execução de comandos via '-c'
  return _executeShell(shell, cmd..redirect)
end

-- ------------------------------------------------------------
-- análoga a execCmd(), mas esta recebe também um número variável de
-- argumentos que serão passados via stdin para o comando

function execCmdWithParams (shell, cmd, logFile, showOutput,...)
  -- arg é a tabela com os argumentos a serem passados para o comando
  -- via stdin
  assert(_availableShells[shell], "shell "..shell.." nao suportado")

  local redirect = _makeRedirection(logFile, showOutput)
  local stdinArgs = table.concat(arg, "\\n")

  if shell == SHELL_BASH then
    cmd = string.format("echo -e '%s' | %s %s", stdinArgs, cmd, redirect)
  elseif shell == SHELL_SH or shell == SHELL_KSH then
    cmd = string.format("echo '%s' | %s %s", stdinArgs, cmd, redirect)
  end
  return _executeShell(shell, cmd)
end

-- ------------------------------------------------------------
-- análoga a execCmd(), mas esta recebe uma tabela com as variáveis de ambiente e um número
-- variável de argumentos que serão passados via stdin para o comando

function execCmdWithEnvVarParams (shell, envVars, cmd, logFile, showOutput,...)
  -- arg é a tabela com os argumentos a serem passados para o comando
  -- via stdin
  assert(_availableShells[shell], "shell "..shell.." nao suportado")

  local exports = ""
  for name, value in pairs(envVars) do
    local export = string.format("%s=%s; export %s; ", name, value, name)
	exports = exports..export
  end

  local redirect = _makeRedirection(logFile, showOutput)
  local stdinArgs = table.concat(arg, "\\n")

  if shell == SHELL_BASH then
	cmd = string.format("%s echo -e '%s' | %s %s", exports, stdinArgs, cmd, redirect)
  elseif shell == SHELL_SH or shell == SHELL_KSH then
	cmd = string.format("%s echo '%s' | %s %s", exports, stdinArgs, cmd, redirect)
  end
  return _executeShell(shell, cmd)
end

-- executa uma linha de comando com os parametros dados
function execCmdLine (shell, cmd, logFile, showOutput,...)
  assert(_availableShells[shell], "shell "..shell.." nao suportado")
  local stdinArgs = table.concat(arg, " ")

  local redirect = _makeRedirection(logFile, showOutput)
  return _executeShell(shell, cmd.." "..stdinArgs.." "..redirect)
end

-- ------------------------------------------------------------
-- para uma tabela com paths, retorna:
--     * prefixo comum a todos (ou "" caso não exista prefixo comum)
--     * nova tabela com paths correspondentes ao original, sem o prefixo
--       comum
-- Ex.: para paths = { "/aaa/bbb/ccc", "/aaa/bbb/ddd", "/aaa/bbb/eee/fff" }
--      o resultado seria:
--      "/aaa/bbb", { "ccc", "ddd", "eee/fff" }

function getCommonPath (paths)
  if (#paths==1) then
    local commonPath, rest = splitPath(paths[1])
    local restTable = {}
    table.insert(restTable, rest)
    return commonPath, restTable
  end
  local commonPath = ""
  -- newPaths é uma cópia _por valor_ de paths
  local newPaths = _tableCopy(paths)
  local referencePath = paths[1]
  repeat
    -- usamos sempre uma nova tabela; desperdiça memória, mas simplifica
    -- processamento (isto garante que a cada iteração newPaths seja
    -- sempre válida)
    local tempTable = {}
    -- pega o 1o componente do path de referência
    local dir
    dir, referencePath = _getFirstDirPart(referencePath)
    local allMatch = true
    for i, path in ipairs(newPaths) do
      -- trataremos o primeiro path novamente, mas isso simplifica o loop
      local d, rest = _getFirstDirPart(path)
      if d ~= dir then
        -- o path em questão não compartilha o 1o componente com os
        -- demais; interrompemos o loop interno e sinalizamos para o
        -- loop externo
        allMatch = false
        break
      end
      -- ok, adiciona o path atual à tabela temporária
      tempTable[i] = rest
    end
    if not allMatch then
      -- algum dos paths não bateu
      break
    end
    -- todos os paths compartilhavam o 1o componente; "oficializa"
    -- tempTable como a nova newPaths
    if commonPath == "" then
        commonPath = dir
    else
        commonPath = commonPath..fileSeparator..dir
    end
    newPaths = tempTable
  until referencePath == ""
  -- commonPath pode estar começando com '//'
  commonPath = strRemoveDuplicates(commonPath, fileSeparator)
--table.setn(newPaths, #paths)
  return commonPath, newPaths
end

-- ------------------------------------------------------------
-- dados um diretório de referência e um path para um arquivo em que
-- ambos compartilham um prefixo comum, retorna menor caminho relativo
-- para se chegar a 'baseDir' a partir do 'path'

function findRelativePath (baseDir, path)
    local commonPath, paths = getCommonPath { shrinkDir(baseDir), path }
    local repl = _fixFileSep("[^/]+", "/")
    local relPath = string.gsub(paths[1], repl, "..") .. fileSeparator ..
        paths[2]
    relPath = strRemoveDuplicates(relPath, fileSeparator)
    -- o path resultante não pode ser absoluto; retiramos o '/' no
    -- começo, caso este exista
    return string.gsub(relPath, "^/?(.*)$", "%1")
end

-- ------------------------------------------------------------
-- cria um arquivo temporário através de mktemp; o path é opcional
-- retorna o nome do arquivo

function mktemp (path)
    if path then
        path = path.."/XXXXXX"
    else
        path = _currDir("XXXXXX")
    end
    local fileHandle = io.popen("mktemp -q '"..path.."'")
    -- temos que usar splitPath porque queremos retornar apenas o nome do
    -- arquivo
    local _, temp = splitPath(fileHandle:read("*a"))
    fileHandle:close()
    -- precisamos remover o \n no final do nome
    return (string.gsub(temp, "%s", ""))
end

-- ---------------------------------------------------------------
-- simula o 'grep': retorna as linhas do arquivo-texto que batem
-- com um determinado padrão. Um terceiro parâmetro (opcional) define se
-- apenas a primeira ocorrência deve ser retornada.
-- Quando apenas um resultado foi encontrado, a linha e retornada. Para
-- multiplas linhas, os resultados são retornados como itens de uma tabela,
-- indexados de acordo com a ordem em que foram encontrados (i.e. result[1]
-- = 1a linha, result[2] = 2a linha etc.)
-- Se nenhuma ocorrencia for encontrada, retorna nil

function grep (arq, pattern, firstOnly)
  local result = {}
  for line in io.lines(_currDir(arq)) do
    if string.find(line, pattern) then
      -- removemos espaços no início e no fim da linha
      local str = string.gsub(line, "%s*(.-)%s*$", "%1")
      table.insert(result, str)
      if firstOnly then return result[1] end
    end
  end
  if #result == 0 then return nil end
  return result 
end

-- ---------------------------------------------------------------
-- Escreve uma mensagem num arquivo dado

function writeMsg (fileName, message)
  local file = fileOpen(fileName, "a+")
  if file~= nil then
    file:write(message.."\n")
    file:close()
  end
end

-- ------------------------------------------------------------
-- Espera um certo numero de segundos
-- RECEBE: O numero de segundos que ira esperar

function sleep (sec)
  _execute("sleep "..sec)
end

-- ------------------------------------------------------------------
-- Cria uma fórmula matemática a partir do valor codificado pelo
-- parâmetro do tipo fórmula.
--
-- Parâmetros:
-- value - O valor codificado.
--
-- Valor de retorno:
-- A fórmula.
-- ------------------------------------------------------------------
function getFormula(value)
  local replacements = {}

  replacements = {
    Space = ' ',
    Underline = '_',
    PlusSign = '+',
    MinusSign = '-',
    Slash = '/',
    Asterisk = '*',
    Percent = '%',
    Dot = '.',
    OpenRoundBracket = '(',
    CloseRoundBracket = ')',
    OpenSquareBracket = '[',
    CloseSquareBracket = ']',
    OpenCurlyBracket = '{',
    CloseCurlyBracket = '}',
    SemiColon = ';',
    LessThan = '<',
    LargeThan = '>',
    EqualsSign = '=',
    Caret = '^',
  }
  
  value = string.gsub(value, "__([%w]*)__", 
    function (key)
      local value = replacements[key]

      if value == nil then
        return "__" .. key .. "__"
      end

      return value
    end)
  return value
end

-- ------------------------------------------------------------------
-- Cria um 'chooser' para auxiliar na seleção de arquivos/diretórios,
-- dentre algumas alternativas. É usado quando precisamos procurar arquivos
-- em alguns diretórios específicos. 
-- A função add permite a inclusão de um diretório como alternativa.
-- A função choose, procura o arquivo/diretório que recebe como parâmetro
-- nos diretórios previamente incluídos. O primeiro diretório que contiver
-- o arquivo é retornado.
-- (Usado na execução dos scripts start/stop/status/collect_execution_data.)
--
-- Valor de retorno:
-- O objeto chooser.
-- ------------------------------------------------------------------
function createDirectoryChooser()
  local obj = {
    dirs = {},
    add = function(self, dir)
      if dir then
        table.insert(self.dirs, dir.."/")
      end
    end,
    choose = function(self, cmd)
      for i, v in pairs(self.dirs) do
        if fileExists(v..cmd) then
          return v
        end
      end
      return nil
    end,
  }
  return obj
end

------------------------------------------------
-- Funções para fazer lock/unlock de arquivos --
------------------------------------------------
local WAITSEC=5
local DONE=".DONE"
local LOCK=".LOCK"
local ERROR=".ERROR"

-- ------------------------------------------------------------------
-- Tenta obter o lock do arquivo/diretório passado como parâmetro.
-- Caso não consiga, espera até que lock seja liberado.
--
-- Parâmetros:
-- file - Arquivo ou diretório do qual se deseja obter o lock.
--
-- Valor de retorno:
--    false : não conseguiu lock OU conseguiu o lock, mas caso já foi
--            processado.
--    true : lock criado e caso ainda não foi processado.
-- ------------------------------------------------------------------
function lockWait (file)
  local lockFile = file..LOCK
  local doneFile = file..DONE
  local errorFile = file..ERROR
  -- Cria diretório de lock. Se der erro, assumimos que é porque outro 
  -- processo está fazendo o processamento, então espera!
  while not mkdir(lockFile) do
    sleep(WAITSEC)
  end
  if (not fileExists(doneFile)) and (not fileExists(errorFile)) then
    --lock criado mas o caso ainda não foi processado
    return true
  end
  unlock(file)
  return false
end

-- ------------------------------------------------------------------
-- Tenta obter o lock do arquivo/diretório passado como parâmetro.
--
-- Parâmetros:
-- file - Arquivo ou diretório do qual se deseja obter o lock.
--
-- Valor de retorno:
--    false : não conseguiu lock OU conseguiu o lock, mas caso já foi
--            processado.
--    true : lock criado e caso ainda não foi processado.
-- ------------------------------------------------------------------
function lock (file)
  local lockFile = file..LOCK
  local doneFile = file..DONE
  local errorFile = file..ERROR
  -- Cria diretório de lock. Se der erro, assumimos que é porque lock já
  -- foi criado por outro processo
  if mkdir(lockFile) then
    if (not fileExists(doneFile)) and (not fileExists(errorFile)) then
      --lock criado mas o caso ainda não foi processado
      return true
    end
    unlock(file)
  end
  return false
end

-- ------------------------------------------------------------------
-- Libera o lock do arquivo/diretório passado como parâmetro.
--
-- Parâmetros:
-- file - Arquivo ou diretório do qual se deseja liberar o lock.
-- done - Indica se deve criar um arquivo DONE, indicando que o caso já
--        foi processado.
-- err - Indica se o arquivo DONE deve conter a extensão de ERRO ao
--       invés de DONE.
-- ------------------------------------------------------------------
function unlock (case, done, err)
  local lockFile = case..LOCK
  local touchFile = { case..DONE, case..ERROR}
  if done then
    if err then
      emptyFile(touchFile[2])
    else
      emptyFile(touchFile[1])
    end
  end
  rmdir(lockFile)
end
