var common = require('./common'); | |
var fs = require('fs'); | |
var path = require('path'); | |
var PERMS = (function (base) { | |
return { | |
OTHER_EXEC : base.EXEC, | |
OTHER_WRITE : base.WRITE, | |
OTHER_READ : base.READ, | |
GROUP_EXEC : base.EXEC << 3, | |
GROUP_WRITE : base.WRITE << 3, | |
GROUP_READ : base.READ << 3, | |
OWNER_EXEC : base.EXEC << 6, | |
OWNER_WRITE : base.WRITE << 6, | |
OWNER_READ : base.READ << 6, | |
// Literal octal numbers are apparently not allowed in "strict" javascript. Using parseInt is | |
// the preferred way, else a jshint warning is thrown. | |
STICKY : parseInt('01000', 8), | |
SETGID : parseInt('02000', 8), | |
SETUID : parseInt('04000', 8), | |
TYPE_MASK : parseInt('0770000', 8) | |
}; | |
})({ | |
EXEC : 1, | |
WRITE : 2, | |
READ : 4 | |
}); | |
//@ | |
//@ ### chmod(octal_mode || octal_string, file) | |
//@ ### chmod(symbolic_mode, file) | |
//@ | |
//@ Available options: | |
//@ | |
//@ + `-v`: output a diagnostic for every file processed//@ | |
//@ + `-c`: like verbose but report only when a change is made//@ | |
//@ + `-R`: change files and directories recursively//@ | |
//@ | |
//@ Examples: | |
//@ | |
//@ ```javascript | |
//@ chmod(755, '/Users/brandon'); | |
//@ chmod('755', '/Users/brandon'); // same as above | |
//@ chmod('u+x', '/Users/brandon'); | |
//@ ``` | |
//@ | |
//@ Alters the permissions of a file or directory by either specifying the | |
//@ absolute permissions in octal form or expressing the changes in symbols. | |
//@ This command tries to mimic the POSIX behavior as much as possible. | |
//@ Notable exceptions: | |
//@ | |
//@ + In symbolic modes, 'a-r' and '-r' are identical. No consideration is | |
//@ given to the umask. | |
//@ + There is no "quiet" option since default behavior is to run silent. | |
function _chmod(options, mode, filePattern) { | |
if (!filePattern) { | |
if (options.length > 0 && options.charAt(0) === '-') { | |
// Special case where the specified file permissions started with - to subtract perms, which | |
// get picked up by the option parser as command flags. | |
// If we are down by one argument and options starts with -, shift everything over. | |
filePattern = mode; | |
mode = options; | |
options = ''; | |
} | |
else { | |
common.error('You must specify a file.'); | |
} | |
} | |
options = common.parseOptions(options, { | |
'R': 'recursive', | |
'c': 'changes', | |
'v': 'verbose' | |
}); | |
if (typeof filePattern === 'string') { | |
filePattern = [ filePattern ]; | |
} | |
var files; | |
if (options.recursive) { | |
files = []; | |
common.expand(filePattern).forEach(function addFile(expandedFile) { | |
var stat = fs.lstatSync(expandedFile); | |
if (!stat.isSymbolicLink()) { | |
files.push(expandedFile); | |
if (stat.isDirectory()) { // intentionally does not follow symlinks. | |
fs.readdirSync(expandedFile).forEach(function (child) { | |
addFile(expandedFile + '/' + child); | |
}); | |
} | |
} | |
}); | |
} | |
else { | |
files = common.expand(filePattern); | |
} | |
files.forEach(function innerChmod(file) { | |
file = path.resolve(file); | |
if (!fs.existsSync(file)) { | |
common.error('File not found: ' + file); | |
} | |
// When recursing, don't follow symlinks. | |
if (options.recursive && fs.lstatSync(file).isSymbolicLink()) { | |
return; | |
} | |
var perms = fs.statSync(file).mode; | |
var type = perms & PERMS.TYPE_MASK; | |
var newPerms = perms; | |
if (isNaN(parseInt(mode, 8))) { | |
// parse options | |
mode.split(',').forEach(function (symbolicMode) { | |
/*jshint regexdash:true */ | |
var pattern = /([ugoa]*)([=\+-])([rwxXst]*)/i; | |
var matches = pattern.exec(symbolicMode); | |
if (matches) { | |
var applyTo = matches[1]; | |
var operator = matches[2]; | |
var change = matches[3]; | |
var changeOwner = applyTo.indexOf('u') != -1 || applyTo === 'a' || applyTo === ''; | |
var changeGroup = applyTo.indexOf('g') != -1 || applyTo === 'a' || applyTo === ''; | |
var changeOther = applyTo.indexOf('o') != -1 || applyTo === 'a' || applyTo === ''; | |
var changeRead = change.indexOf('r') != -1; | |
var changeWrite = change.indexOf('w') != -1; | |
var changeExec = change.indexOf('x') != -1; | |
var changeSticky = change.indexOf('t') != -1; | |
var changeSetuid = change.indexOf('s') != -1; | |
var mask = 0; | |
if (changeOwner) { | |
mask |= (changeRead ? PERMS.OWNER_READ : 0) + (changeWrite ? PERMS.OWNER_WRITE : 0) + (changeExec ? PERMS.OWNER_EXEC : 0) + (changeSetuid ? PERMS.SETUID : 0); | |
} | |
if (changeGroup) { | |
mask |= (changeRead ? PERMS.GROUP_READ : 0) + (changeWrite ? PERMS.GROUP_WRITE : 0) + (changeExec ? PERMS.GROUP_EXEC : 0) + (changeSetuid ? PERMS.SETGID : 0); | |
} | |
if (changeOther) { | |
mask |= (changeRead ? PERMS.OTHER_READ : 0) + (changeWrite ? PERMS.OTHER_WRITE : 0) + (changeExec ? PERMS.OTHER_EXEC : 0); | |
} | |
// Sticky bit is special - it's not tied to user, group or other. | |
if (changeSticky) { | |
mask |= PERMS.STICKY; | |
} | |
switch (operator) { | |
case '+': | |
newPerms |= mask; | |
break; | |
case '-': | |
newPerms &= ~mask; | |
break; | |
case '=': | |
newPerms = type + mask; | |
// According to POSIX, when using = to explicitly set the permissions, setuid and setgid can never be cleared. | |
if (fs.statSync(file).isDirectory()) { | |
newPerms |= (PERMS.SETUID + PERMS.SETGID) & perms; | |
} | |
break; | |
} | |
if (options.verbose) { | |
log(file + ' -> ' + newPerms.toString(8)); | |
} | |
if (perms != newPerms) { | |
if (!options.verbose && options.changes) { | |
log(file + ' -> ' + newPerms.toString(8)); | |
} | |
fs.chmodSync(file, newPerms); | |
} | |
} | |
else { | |
common.error('Invalid symbolic mode change: ' + symbolicMode); | |
} | |
}); | |
} | |
else { | |
// they gave us a full number | |
newPerms = type + parseInt(mode, 8); | |
// POSIX rules are that setuid and setgid can only be added using numeric form, but not cleared. | |
if (fs.statSync(file).isDirectory()) { | |
newPerms |= (PERMS.SETUID + PERMS.SETGID) & perms; | |
} | |
fs.chmodSync(file, newPerms); | |
} | |
}); | |
} | |
module.exports = _chmod; |