map_install_src.lua 4.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
#!/usr/bin/env luajit

-- parse install commands from stdin
-- input: PREFIX=... make install --dry-run --always-make
-- output: <install path> <source path>
-- (or sed commands if --sed was specified)

output = 'list'
if #arg > 1 or arg[1] == '-h' or arg[1] == '--help' then
	print(string.format([[
Read install commands and map install paths to paths in source directory.

Usage:
$ PREFIX=... make install --dry-run --always-make | %s

Example output:
/kresd/git/.local/lib/kdns_modules/policy.lua	modules/policy/policy.lua

Option --sed will produce output suitable as input suitable for sed.]],
				arg[0]))
	os.exit(1)
elseif #arg == 0 then
	output = 'list'
elseif arg[1] == '--sed' then
	output = 'sed'
else
	print('Invalid arguments. See --help.')
	os.exit(2)
end

-- remove double // from paths and remove trailing /
function normalize_path(path)
	assert(path)
	repeat
		path, changes = path:gsub('//', '/')
	until changes == 0
	return path:gsub('/$', '')
end

function is_opt(word)
	return word:match('^-')
end

-- opts requiring additional argument to be skipped
local ignored_opts_with_arg = {
	['--backup'] = true,
	['-g'] = true,
	['--group'] = true,
	['-m'] = true,
	['--mode'] = true,
	['-o'] = true,
	['--owner'] = true,
	['--strip-program'] = true,
	['--suffix'] = true,
}

-- state machine junctions caused by --opts
-- returns: new state (expect, mode) and target name if any
function parse_opts(word, expect, mode)
	if word == '--' then
		return 'names', mode, nil -- no options anymore
	elseif word == '-d' or word == '--directory' then
		return 'opt_or_name', 'newdir', nil
	elseif word == '-t' or word == '--target-directory' then
		return 'targetdir', mode, nil
	elseif word:match('^--target-directory=') then
		return 'opt_or_name', mode, string.sub(word, 20)
	elseif ignored_opts_with_arg[word] then
		return 'ignore', mode, nil -- ignore next word
	else
		return expect, mode, nil -- unhandled opt
	end
end


-- cmd: complete install command line: install -m 0644 -t dest src1 src2
-- dirs: names known to be directories: name => true
-- returns: updated dirs
function process_cmd(cmd, dirs)
	-- print('# ' .. cmd)
	sanity_check(cmd)
	local expect = 'install'
	local mode = 'copy' -- copy or newdir
	local target -- last argument or argument for install -t
	local names = {} -- non-option arguments

	for word in cmd:gmatch('%S+') do
		if expect == 'install' then -- parsing 'install'
			assert(word == 'install')
			expect = 'opt_or_name'
		elseif expect == 'opt_or_name' then
			if is_opt(word) then
				expect, mode, newtarget = parse_opts(word, expect, mode)
				target = newtarget or target
			else
				if mode == 'copy' then
					table.insert(names, word)
				elseif mode == 'newdir' then
					local path = normalize_path(word)
					dirs[path] = true
				else
					assert(false, 'bad mode')
				end
			end
		elseif expect == 'targetdir' then
			local path = normalize_path(word)
			dirs[path] = true
			target = word
			expect = 'opt_or_name'
		elseif expect == 'names' then
			table.insert(names, word)
		elseif expect == 'ignore' then
			expect = 'opt_or_name'
		else
			assert(false, 'bad expect')
		end
	end
	if mode == 'newdir' then
		-- no mapping to print, this cmd just created directory
		return dirs
	end

	if not target then -- last argument is the target
		target = table.remove(names)
	end
	assert(target, 'fatal: no target in install cmd')
	target = normalize_path(target)

	for _, name in pairs(names) do
		basename = string.gsub(name, "(.*/)(.*)", "%2")
		if not dirs[target] then
			print('fatal: target directory "' .. target .. '" was not created yet!')
			os.exit(2)
		end
		-- mapping installed name -> source name
		if output == 'list' then
			print(target .. '/' .. basename, name)
		elseif output == 'sed' then
			print(string.format([[s`%s`%s`g]],
					    target .. '/' .. basename, name))
		else
			assert(false, 'unsupported output')
		end
	end
	return dirs
end

function sanity_check(cmd)
	-- shell quotation is not supported
	assert(not cmd:match('"'), 'quotes " are not supported')
	assert(not cmd:match("'"), "quotes ' are not supported")
	assert(not cmd:match('\\'), "escapes like \\ are not supported")
	assert(cmd:match('^install%s'), 'not an install command')
end

-- remember directories created by install -d so we can expand relative paths
local dirs = {}
while true do
	local cmd = io.read("*line")
	if not cmd then
		break
	end
	local isinstall = cmd:match('^install%s')
	if isinstall then
		dirs = process_cmd(cmd, dirs)
	end
end