#!/usr/bin/env texlua -- ****************************************************************** -- checkcites.lua -- Copyright 2012 Enrico Gregorio, Paulo Roberto Massa Cereda -- -- This work may be distributed and/or modified under the -- conditions of the LaTeX Project Public License, either version 1.3 -- of this license or (at your option) any later version. -- -- The latest version of this license is in -- http://www.latex-project.org/lppl.txt -- and version 1.3 or later is part of all distributions of LaTeX -- version 2005/12/01 or later. -- -- This work has the LPPL maintenance status `maintained'. -- -- The Current Maintainers of this work are the original authors. -- -- This work consists of the file checkcites.lua. -- -- Project page: http://github.com/cereda/checkcites -- ****************************************************************** -- version and date, to be updated on each release/commit VERSION = "1.0i" DATE = "December 18, 2012" -- globals -- warning about \citation{*} globalAsteriskWarning = true -- The following code adds a 'split' function to the string type, -- thanks to the codebase available here: -- http://lua-users.org/wiki/SplitJoin string.split = function(str, pattern) pattern = pattern or "[^%s]+" if pattern:len() == 0 then pattern = "[^%s]+" end local parts = {__index = table.insert} setmetatable(parts, parts) str:gsub(pattern, parts) setmetatable(parts, nil) parts.__index = nil return parts end -- In order to make our lives easier, we borrowed the following -- codes for implementing common set operations, available here: -- http://www.phailed.me/2011/02/common-set-operations-in-lua/ function setFind(a, tbl) for _,a_ in ipairs(tbl) do if a_==a then return true end end end -- This code returns a table containing the difference between -- the two other tables, also available here: -- http://www.phailed.me/2011/02/common-set-operations-in-lua/ function setDifference(a, b) local ret = {} for _,a_ in ipairs(a) do if not setFind(a_,b) then table.insert(ret, a_) end end return ret end -- Code to remove duplicates from a table array. function removeDuplicates(tb) -- new table local ret = {} -- flag to spot new insertions local flag -- set local variables local i local k local j local l -- iterate through the original table for i, k in pairs(tb) do -- at first, insert element flag = true -- iterate through the new table for j, l in pairs(ret) do -- if the element already exists if k == l then -- set flag to false, so the -- new element won't be inserted flag = false end end -- if it's ok to insert if flag then -- insert new element table.insert(ret, k) end end -- return new table return ret end -- This function opens and gets all data from the provided -- aux file. -- Return: -- * list of citations -- * list of bibfiles function getDataFromAuxFile(theAuxFile) -- open a file handler local fileHandler = io.open(theAuxFile,"r") -- check if the handler is valid if fileHandler then -- create a local table for citations local theCitations = {} -- and a local reference to the bib file local theBibFiles = {} -- define local variables local currentLine local citation local index local theCitation local theBibFile local entry -- now let's iterate through the lines for currentLine in fileHandler:lines() do -- if the citation matches, extract it for citation in string.gmatch(currentLine, '\\citation{(.+)}') do -- sanity check, in case it's an '*' if citation ~= "*" then -- break the citations list, in case of multiple -- citations in the same \citation{} local parts = string.split(citation, "[^,%s]+" ) -- for every entry in the citations list for index, theCitation in pairs(parts) do -- insert the reference table.insert(theCitations, theCitation) end -- found a '*' else -- check if warning is still valid, that is, -- if not displayed yet if globalAsteriskWarning then -- show message print("Warning: '\\nocite{*}' found, I'll do the check nonetheless.\n") -- warning already displayed, so -- set flag to false globalAsteriskWarning = false end end -- end of citation in current line end -- in the same current line, check if there's the -- bibdata entry and extract it for entry in string.gmatch(currentLine, '\\bibdata{(.+)}') do -- break the bib files list, in case of multiple -- files in the same \bibdata{} local parts = string.split(entry, "[^,%s]+" ) -- for every entry in the bib files list for index, theBibFile in pairs(parts) do -- insert the file table.insert(theBibFiles, theBibFile) end -- end of bib files in the current line end -- end of current line end -- close the file handler fileHandler:close() -- remove duplicated citations theCitations = removeDuplicates(theCitations) -- print a message about the citations print("I found " .. #theCitations .. " citation(s).") -- remove possible duplicated files theBibFiles = removeDuplicates(theBibFiles) -- if there are no bib files if #theBibFiles == 0 then -- show message print("I couldn't find any bibliography files.\nI'm afraid I have nothing to do now.") -- and abort the script os.exit() -- if there is only one bib file elseif #theBibFiles == 1 then -- show message print("Great, there's only one 'bib' file. Let me check it.") -- there are more bib files else -- show message print("Oh no, I have to check more than one 'bib' file. Please wait.") end -- return both citations and bib files return theCitations, theBibFiles -- the file handler is invalid else -- print an error message print("File '" .. theAuxFile .. "' does not exist or is unavailable. Aborting script.") -- and abort the script os.exit() end -- end of function end -- This function opens and gets all data from all the available -- bib files. function getDataFromBibFiles(theBibFiles) -- create a table to store the citations local theReferences = {} -- set local variables local index local theBibFile local currentLine local reference -- iterate through all bib files for index, theBibFile in pairs(theBibFiles) do -- open the bib file local fileHandler = io.open(theBibFile .. ".bib","r") -- check if the handler is valid if fileHandler then -- iterate through every line of the bib file for currentLine in fileHandler:lines() do -- if a reference is found for reference in string.gmatch(currentLine, '@%w+{%s*(.+),') do -- insert the reference table.insert(theReferences, reference) end -- end current line end -- close the file handler fileHandler:close() -- bib file does not exist else -- error message print("File '" .. theBibFile .. ".bib' does not exist. Aborting.") -- abort script os.exit() end -- end iteration through the bib files end -- remove duplicated references theReferences = removeDuplicates(theReferences) -- print message print("I found " .. #theReferences .. " reference(s).") -- return references return theReferences -- end of function end -- This function show all the undefined references. It's very -- simple, it's a difference between two sets. function showUndefinedReferences(citations, references) -- get all undefined references local undefined = setDifference(citations, references) -- print message print("\nUndefined reference(s) in your TeX file: " .. #undefined) -- if there are undefined references if #undefined ~= 0 then -- local variables local index local reference -- iterate for index, reference in pairs(undefined) do -- and print print("- " .. reference) end end -- end of function end -- This function show all the unused references. It's very -- simple, it's a difference between two sets. function showUnusedReferences(citations, references) -- get all undefined references local unused = setDifference(references, citations) -- print message print("\nUnused reference(s) in your bibliography file(s): " .. #unused) -- if there are unused references if #unused ~= 0 then -- local variables local index local reference -- iterate for index, reference in pairs(unused) do -- and print print("- " .. reference) end end -- end of function end -- This function parses the command line arguments and returns a -- bunch of info for us to use. -- Return: -- * an argument code -- * the filename -- * an action code function parseArguments(theArgs) -- check if there are no arguments if #theArgs == 0 then -- return usage code return 0, nil, nil -- there is one argument elseif #theArgs == 1 then -- check if it's help if theArgs[1] == "--help" then -- return help code return 1, nil, nil -- check if it's version elseif theArgs[1] == "--version" then -- return version code return 2, nil, nil -- check if it's invalid elseif string.sub(theArgs[1], 1, 1) == "-" then -- return invalid code return 3, nil, nil -- it seems a clean argument else -- return it as a valid argument return 4, theArgs[1], nil -- end for one parameter end -- there are two arguments elseif #theArgs == 2 then -- check if both are valid if ((theArgs[1] == "--all" or theArgs[1] == "--unused" or theArgs[1] == "--undefined") and string.sub(theArgs[2], 1, 1) ~= "-") or ((theArgs[2] == "--all" or theArgs[2] == "--unused" or theArgs[2] == "--undefined") and string.sub(theArgs[1], 1, 1) ~= "-") then -- create an action code local actionCode -- check which one is the file name if string.sub(theArgs[1], 1, 1) ~= "-" then -- check for --all if theArgs[2] == "--all" then -- set the action code actionCode = 0 -- check for --unused elseif theArgs[2] == "--unused" then -- set the action code actionCode = 1 -- it's --undefined else -- set the action code actionCode = 2 end -- it's the first return 4, theArgs[1], actionCode else -- check for --all if theArgs[1] == "--all" then -- set the action code actionCode = 0 -- check for --unused elseif theArgs[1] == "--unused" then -- set the action code actionCode = 1 else -- it's --undefined actionCode = 2 end -- it's the second return 4, theArgs[2], actionCode end else -- return invalid code return 3, nil, nil end else -- more than two arguments, return usage code return 0, nil, nil end -- end of function end -- This function prints the script header. function printHeader() -- print message print("checkcites.lua -- a reference checker script (v" .. VERSION .. ")") print("Copyright (c) 2012 Enrico Gregorio, Paulo Roberto Massa Cereda\n") -- end of function end -- This function prints the script usage function printUsage() -- show message print("Usage: " .. arg[0] .. " [--all | --unused | --undefined] file.aux\n") print("--all Lists all unused and undefined references.") print("--unused Lists only unused references in your 'bib' file.") print("--undefined Lists only undefined references in your 'tex' file.\n") print("If no flag is provided, '--all' is set by default.") print("Be sure to have all your 'bib' files in the same directory.") -- end of function end function printHelp() -- show message print("checkcites.lua is a Lua script written for the sole purpose of") print("detecting undefined/unused references from LaTeX auxiliary or") print("bibliography files. It's very easy to use!\n") -- print usage printUsage() -- end of function end -- This function prints the script version. function printVersion() -- print message print("checkcites.lua version " .. VERSION .. " (dated " .. DATE .. ")") print("You can find more information about this script in the official") print("source code repository:\n") print("http://github.com/cereda/checkcites\n") print("checkcites.lua is licensed under the LaTeX Project Public License.") -- end of function end -- This function prints a message for invalid parameters. function printInvalid() -- print message print("Oh no, it seems you used an invalid argument.\n") -- print usage printUsage() -- end of function end -- This is our main function. function runMain(theArgs) -- print the script header printHeader() -- set local variables local argCode local fileName local actionCode -- parse arguments and get the result argCode, fileName, actionCode = parseArguments(theArgs) -- check for usage if argCode == 0 then -- print usage printUsage() -- it's help elseif argCode == 1 then -- print help printHelp() -- it's version elseif argCode == 2 then -- print version printVersion() -- it's an invalid parameter elseif argCode == 3 then -- print invalid printInvalid() -- it's a valid operation else -- get data from aux a, b = getDataFromAuxFile(fileName) -- get data from bib c = getDataFromBibFiles(b) -- if there is an action code if actionCode ~= nil then -- it's --all if actionCode == 0 then -- do everything showUnusedReferences(a,c) showUndefinedReferences(a,c) -- it's --unused elseif actionCode == 1 then -- only show unused showUnusedReferences(a,c) -- it's --undefined else -- only show undefined showUndefinedReferences(a,c) end -- there's only one parameter, the file name, -- so we set --all else -- show everything showUnusedReferences(a,c) showUndefinedReferences(a,c) end end -- end of function end -- run the main function runMain(arg)