Touch Utility for Windows





5.00/5 (3 votes)
A utility program to update the last-accessed time and last-modified time attributes of files and directories
Introduction
Touch is a utility program in Unix-like systems used to change the last-accessed time and last-modified time attributes of files and directories. It creates the file (directory) if it does not exist. I usually use it to create empty files and directory structures for projects I am working on. This utility, unfortunately, doesn't come with Windows. So I created my own. I hope it will be useful to you too.
Using the Code
The following illustrates basic usage of the utility:
> touch afile.cs *.html
Change the last-accessed-time of afile.cs, and all files with .html extension in the current directory, to the current date and time. If afile.cs does not exist, it will be created.
> touch -m --date="Jan 1, 2017 9:15 am" workspace/afile.cs
Change the last-modified-time of afile.cs to the specified date and time. Create it, along with its path, if it does not exist.
> touch -c -a -m --reference=workspace/afile.cs workspace/project/
Change the last-accessed-time and last-modified-time of project
directory, if it does exist, to the corresponding dates and times of afile.cs.
To view usage information, type: touch --help
.
Usage: touch.exe [OPTION]... FILE...
Update the access and modification times of each FILE to the current time.
Mandatory arguments to long options are mandatory for short options too.
-a change only the access time
-c, --no-create do not create any files
-d, --date=STRING parse STRING and use it instead of current time
-f (ignored)
-m change only the modification time
-r, --reference=FILE use this file's times instead of current time
-t STAMP use [[CC]YY]MMDDhhmm[.ss] instead of current time
--time=WORD change the specified time:
WORD is access, atime, or use: equivalent to -a
WORD is modify or mtime: equivalent to -m
--help display this help and exit
--version output version information and exit
Note that the -d and -t options accept different time-date formats.
If a FILE is -, touch standard output.
Describing the Code
The application consists of two classes: The CommandLineParser
class, used to process the command line arguments and validate the options provided, and the Program
class, which uses those options to properly update dates and times.
To compile:
> csc /out:touch.exe CommandLineParser.cs Program.cs
The Command Line Parser
The CommandLineParser
class is based on the class with the same name created by Mike Ellison to parse command lines having Microsoft-style options format (e.g. /t, /out:filename, ...). This class is a modified version, that uses the Unix-style options format (e.g. -t
, --out=filename
, ...), and adds options validation. Essentially, this class parses the command line arguments, and stores it in a Dictionary
of arguments and values, for further processing. Optionally, you might include a list of valid options, and a list of the options that should be set by the user. This class then will validate the arguments against the provided options.
This is the class constructor:
public CommandLineParser(string[] args,
List<string> options = null,
List<string> valuedOptions = null) {
_argsDict = new Dictionary<string, string>();
int i = 0;
while(i < args.Length) {
string s = args[i];
// if it is an option
if (s.StartsWith("-") && (s.Length > 1)) {
// if it is a long option
if (s.StartsWith("--")) {
int j = s.IndexOf("=");
if (j == -1) {
if ((valuedOptions != null) && valuedOptions.Contains(s)) {
if ((i + 1) < args.Length) _argsDict.Add(s, args[++i]);
else throw new Exception("option " + s + " requires an argument");
}
else _argsDict.Add(s, "");
}
else {
string v = s.Substring(j + 1).Trim();
s = s.Substring(0, j).Trim();
_argsDict.Add(s, v);
}
}
// if it is a short option
else if (s.Length == 2) {
if ((valuedOptions != null) && valuedOptions.Contains(s)) {
if ((i + 1) < args.Length) _argsDict.Add(s, args[++i]);
else throw new Exception("option " + s + " requires an argument");
}
else _argsDict.Add(s, "");
}
// if it is a flag of multiple options
else {
Slice(s, options, valuedOptions);
i++;
continue;
}
// is it a legitimate option?
if ((options != null) && !options.Contains(s))
throw new Exception("invalid option " + s);
// does it have a value when it should not?
if ((options != null) && (valuedOptions != null)
&& !valuedOptions.Contains(s) && (GetValue(s) != ""))
throw new Exception("option " + s + " should have no arguments");
}
// if it is not an option then it is a filename
else _argsDict.Add(s, "<FILE>");
i++;
}
}
As you can see, the class stores the arguments and their values in a Dictionary
. The keys can be accessed using the Arguments
property:
public ICollection<string> Arguments {
get { return _argsDict.Keys; }
}
Then we can iterate through the collection of keys, and retrieve the values using the GetValue()
method. The Slash()
method is there to accommodate a short-cut use by combining multiple options in one flag. It allows the user to write as part of the command line arguments, for example, -abc
, which means -a -b -c
or -mofilename
to mean -m -o filename
.
private void Slice(string s, List<string> options, List<string> vOptions) {
if (options == null) {
for (int i = 1; i < s.Length; i++)
_argsDict.Add("-" + s.Substring(i, 1), "");
return;
}
string v = s.Substring(2).Trim();
s = s.Substring(0, 2).Trim();
if ((vOptions != null) && (vOptions.Contains(s)) ||
((options != null) && (!options.Contains("-" + v.Substring(0, 1)))))
_argsDict.Add(s, v);
else {
_argsDict.Add(s, "");
Slice("-" + v, options, vOptions);
}
}
This class is built as an independent component. You can use it in your own program, if you want to capture options from, or have options set via, the command line, which is the case for most utility programs. In the next section, we will see how to do just that.
The Main Program
The program uses the CommandLineParser
to process the command line flags, and use the resulting dictionary of options and values to determine two variables:
mode
, with possible values:accessOnly
(update the file’s last-accessed attribute),modifyOnly
(update the file's last-modified attribute), or both. The default isaccessOnly
.dateTime
, the actual date and time to be used in updating the files' attributes. There are three possible sources for the valuedateTime
: fromtime-stamp
(provided in the command line through the flag-t
), from traditional date format (through flags-d
or--date
), or by specifying an existing file (using-r
or--reference
flags), and the needed date and time will be copied from the file's attributes. The default isDateTime.Now
, which is the computer clock.
The main
method first defines a list of allowed flags The program uses to set the various options and then handles simple requests for help
and version
.
static void Main(string[] args) {
List<string> options = new List<string> {
"--no_create", "--date", "--time", "--reference", "--help", "--ver",
"--version", "--h", "-a", "-c", "-d", "-f", "-m", "-r", "-t"
};
List<string> valuedOptions = new List<string> {
"--date", "--time", "--reference", "-d", "-t", "-r"
};
List<string> accessMode = new List<string> { "access", "atime", "use" };
List<string> modifyMode = new List<string> { "modify", "mtime" };
Mode mode = Mode.nothing;
TimeSetter timeSetter = TimeSetter.now;
string reference = null;
bool refIsDir = false;
DateTime dateTime = DateTime.Now;
// if asked for 'help' or 'version', just print the message and exit
foreach(string s in args) {
if(s == "--help" || s == "--h") {
Console.WriteLine(helpMsg);
return;
}
if (s == "--version" || s == "--ver") {
Console.WriteLine(versionMsg);
return;
}
}
Next, in the main processing block, the dateTime
and mode
variables are set, and the Touch()
method is called for every filename in the command line:
try {
CommandLineParser parser = new CommandLineParser(args, options, valuedOptions);
foreach (string s in parser.Arguments) {
switch (s) {
case "-t":
// use time-stamp format
// eg. 201702142200.00 which means 14 feb. 2017 10:00 pm
if(timeSetter != TimeSetter.now)
throw new Exception("cannot specify times from more than one source");
timeSetter = TimeSetter.stamp;
dateTime = GetDateTime(parser.GetValue(s));
break;
case "-d":
case "--date":
// use date format
// eg. "May 20, 1999 8:35 AM", 2016/12/25, "2 October", ..etc.
if(timeSetter != TimeSetter.now)
throw new Exception("cannot specify times from more than one source");
timeSetter = TimeSetter.date;
try {
dateTime = DateTime.Parse(parser.GetValue(s));
} catch (Exception) {
throw new Exception("invalid date format '" + parser.GetValue(s) + "'");
}
break;
case "-r":
case "--reference":
// use the time of an existing reference file
if (timeSetter != TimeSetter.now) throw new Exception(
"cannot specify times from more than one source"
);
timeSetter = TimeSetter.fromFile;
reference = parser.GetValue(s);
if (!File.Exists(reference)) {
if (!Directory.Exists(reference))
throw new Exception("reference '" +
reference + "': No such file or directory");
else refIsDir = true;
}
break;
default:
// Check what to update: access time, modification time or both.
// if no flags provided, only the access time will be updated.
if(s == "-a" || (s == "--time" &&
accessMode.Contains(parser.GetValue(s)))) {
if (mode == Mode.modifyOnly) mode = Mode.both;
else if (mode == Mode.nothing) mode = Mode.accessOnly;
}
if (s == "-m" || (s == "--time" &&
modifyMode.Contains(parser.GetValue(s)))) {
if (mode == Mode.accessOnly) mode = Mode.both;
else if (mode == Mode.nothing) mode = Mode.modifyOnly;
}
if(s == "--time" &&
!accessMode.Contains(s) &&
!modifyMode.Contains(parser.GetValue(s)))
throw new Exception(parser.GetValue(s) +
" is not valid argument for --time");
break;
}
}
// Check if we actually have any files or directories to update
if (!parser.HasKey("-") && !parser.HasValue("<FILE>"))
throw new Exception("missing file operand");
// setting times for STDOUT? Sorry. Not at the moment.
if (parser.HasKey("-")) {
Console.WriteLine("touch: setting times of '-': Function not implemented");
return;
}
// All clear: We are out of excuses. Let's go and 'touch' some files
foreach (string s in parser.Arguments) {
if (parser.GetValue(s) == "<FILE>") {
bool create = !parser.HasKey("--no_create") && !parser.HasKey("-c");
try {
Touch(s, mode, dateTime, reference, refIsDir, create);
} catch (Exception ex) {
// Access denied, ... etc.
Console.WriteLine(ex.Message);
}
}
}
} catch (Exception ex) {
Console.WriteLine("touch: " + ex.Message);
Console.WriteLine("Try 'touch --help' for more information");
}
GetDateTime()
is a method that takes a time-stamp (in the form [[CC]YY]MMddhhmm[.ss]
) and converts it to .NET DateTime
class.
static DateTime GetDateTime(string timeStamp) {
string format = timeStamp;
int year, month, day, hour, minute, second;
try {
// Throws an exception if it is not a number
Double.Parse(timeStamp);
// Remember the format is: [[CC]YY]MMDDhhmm[.ss]
if ((format.Length == 15) || format.Length == 12) {
year = int.Parse(format.Substring(0, 4));
format = format.Substring(4);
}
else if ((format.Length == 13) || format.Length == 10) {
year = int.Parse("20" + format.Substring(0, 2));
if (year > DateTime.Now.Year) year -= 100;
format = format.Substring(2);
}
else year = DateTime.Now.Year;
if (format.Length == 11 || format.Length == 8) {
month = int.Parse(format.Substring(0, 2));
day = int.Parse(format.Substring(2, 2));
hour = int.Parse(format.Substring(4, 2));
minute = int.Parse(format.Substring(6, 2));
format = format.Substring(8);
if (format.Length == 0) second = 0;
else if (format.StartsWith("."))
second = int.Parse(format.Substring(1, 2));
else throw new Exception();
}
else throw new Exception();
return new DateTime(year, month, day, hour, minute, second);
}
catch (Exception) { throw new Exception("invalid date format '" + timeStamp + "'"); }
}
The Touch
method is where the actual updating/creation of files occur. First, we break the path into a directory part and a file part, check the existence of each:
static void Touch(string path, Mode mode, DateTime dateTime,
string fileRef = null, bool refIsDir = false, bool create = true) {
string baseDir, pattern;
baseDir = Path.GetDirectoryName(path);
if (baseDir == "") baseDir = ".";
pattern = Path.GetFileName(path);
// The directory part does not exist. Create it?
if (!Directory.Exists(baseDir)) {
if (create) Directory.CreateDirectory(baseDir);
else return;
}
// The file part does not exist. Create it?
if (!File.Exists(path) && !Directory.Exists(path) &&
(!(path.Contains("*") || path.Contains("?"))) && pattern != "") {
if (create) File.Create(path).Close();
else return;
}
if (pattern == "") {
pattern = baseDir.Substring(baseDir.LastIndexOf("\\") + 1);
baseDir = Directory.GetParent(baseDir).ToString();
}
Finally, we update the desired attributes of the directories and files.
// Update the directories
foreach (string p in Directory.GetDirectories(baseDir, pattern)) {
switch (mode) {
case Mode.accessOnly:
if (fileRef != null) dateTime = refIsDir? Directory.GetLastAccessTime(fileRef)
: File.GetLastAccessTime(fileRef);
Directory.SetLastAccessTime(p, dateTime);
break;
case Mode.modifyOnly:
if (fileRef != null) dateTime = refIsDir? Directory.GetLastAccessTime(fileRef)
: File.GetLastWriteTime(fileRef);
Directory.SetLastWriteTime(p, dateTime);
break;
default:
if (fileRef != null) {
Directory.SetLastAccessTime(p, refIsDir? Directory.GetLastAccessTime(fileRef)
: File.GetLastAccessTime(fileRef));
Directory.SetLastWriteTime(p, refIsDir? Directory.GetLastAccessTime(fileRef)
: File.GetLastWriteTime(fileRef));
}
else {
Directory.SetLastAccessTime(p, dateTime);
Directory.SetLastWriteTime(p, dateTime);
}
break;
}
}
// Update the files
foreach (string p in Directory.GetFiles(baseDir, pattern)) {
switch (mode) {
case Mode.accessOnly:
if (fileRef != null) dateTime = refIsDir? Directory.GetLastAccessTime(fileRef)
: File.GetLastAccessTime(fileRef);
File.SetLastAccessTime(p, dateTime);
break;
case Mode.modifyOnly:
if (fileRef != null) dateTime = refIsDir? Directory.GetLastAccessTime(fileRef)
: File.GetLastWriteTime(fileRef);
File.SetLastWriteTime(p, dateTime);
break;
default:
if (fileRef != null) {
File.SetLastAccessTime(p, File.GetLastAccessTime(fileRef));
File.SetLastWriteTime(p, File.GetLastWriteTime(fileRef));
}
else {
File.SetLastAccessTime(p, dateTime);
File.SetLastWriteTime(p, dateTime);
}
break;
}
}
That's all folks. Enjoy!
History
- 15th May, 2019: Initial version