Saturday, September 23, 2023
Monday, September 18, 2023
The following is written by Brian Zahnd.
I have a problem with the Bible. Here’s my problem…
I’m an ancient Egyptian. I’m a comfortable Babylonian. I’m a Roman in his villa.
That’s my problem. See, I’m trying to read the Bible for all it’s worth, but I’m not a Hebrew slave suffering in Egypt. I’m not a conquered Judean deported to Babylon. I’m not a first century Jew living under Roman occupation.
I’m a citizen of a superpower. I was born among the conquerors. I live in the empire. But I want to read the Bible and think it’s talking to me. This is a problem.
One of the most remarkable things about the Bible is that in it we find the narrative told from the perspective of the poor, the oppressed, the enslaved, the conquered, the occupied, the defeated. This is what makes it prophetic. We know that history is written by the winners. This is true — except in the case of the Bible it’s the opposite! This is the subversive genius of the Hebrew prophets. They wrote from a bottom-up perspective.
Imagine a history of colonial America written by Cherokee Indians and African slaves. That would be a different way of telling the story! And that’s what the Bible does. It’s the story of Egypt told by the slaves. The story of Babylon told by the exiles. The story of Rome told by the occupied. What about those brief moments when Israel appeared to be on top? In those cases the prophets told Israel’s story from the perspective of the peasant poor as a critique of the royal elite. Like when Amos denounced the wives of the Israelite aristocracy as “the fat cows of Bashan.”
Every story is told from a vantage point; it has a bias. The bias of the Bible is from the vantage point of the underclass. But what happens if we lose sight of the prophetically subversive vantage point of the Bible? What happens if those on top read themselves into the story, not as imperial Egyptians, Babylonians, and Romans, but as the Israelites? That’s when you get the bizarre phenomenon of the elite and entitled using the Bible to endorse their dominance as God’s will. This is Roman Christianity after Constantine. This is Christendom on crusade. This is colonists seeing America as their promised land and the native inhabitants as Canaanites to be conquered. This is the whole history of European colonialism. This is Jim Crow. This is the American prosperity gospel. This is the domestication of Scripture. This is making the Bible dance a jig for our own amusement.
As Jesus preached the arrival of the kingdom of God he would frequently emphasize the revolutionary character of God’s reign by saying things like, “the last will be first and the first last.” How does Jesus’ first-last aphorism strike you? I don’t know about you, but it makes this modern day Roman a bit nervous.
Imagine this: A powerful charismatic figure arrives on the world scene and amasses a great following by announcing the arrival of a new arrangement of the world where those at the bottom are to be promoted and those on top are to have their lifestyle “restructured.” How do people receive this? I can imagine the Bangladeshis saying, “When do we start?!” and the Americans saying, “Hold on now, let’s not get carried away!”
Now think about Jesus announcing the arrival of God’s kingdom with the proclamation of his counterintuitive Beatitudes. When Jesus said, “Blessed are the meek, for they shall inherit the earth,” how was that received? Well, it depends on who is hearing it. The poor Galilean peasant would hear it as good news (gospel), while the Roman in his villa would hear it with deep suspicion. (I know it’s an anachronism, but I can imagine Claudius saying something like, “sounds like socialism to me!”)
And that’s the challenge I face in reading the Bible. I’m not the Galilean peasant. Who am I kidding! I’m the Roman in his villa and I need to be honest about it. I too can hear the gospel of the kingdom as good news (because it is!), but first I need to admit its radical nature and not try to tame it to endorse my inherited entitlement.
I am a (relatively) wealthy white American male. Which is fine, but it means I have to work hard at reading the Bible right. I have to see myself basically as aligned with Pharaoh, Nebuchadnezzar, and Caesar. In that case, what does the Bible ask of me? Voluntary poverty? Not necessarily. But certainly the Bible calls me to deep humility — a humility demonstrated in hospitality and generosity. There’s nothing necessarily wrong with being a relatively well-off white American male, but I better be humble, hospitable, and generous!
If I read the Bible with the appropriate perspective and humility I don’t use the story of the Rich Man and Lazarus as a proof-text to condemn others to hell. I use it as a reminder that I’m a rich man and Lazarus lies at my door. I don’t use the conquest narratives of Joshua to justify Manifest Destiny. Instead I see myself as a Rahab who needs to welcome newcomers. I don’t fancy myself as Elijah calling down fire from heaven. I’m more like Nebuchadnezzar who needs to humble himself lest I go insane.
I have a problem with the Bible, but all is not lost. I just need to read it standing on my head. I need to change my perspective. If I can accept that the Bible is trying to lift up those who are unlike me, then perhaps I can read the Bible right.”
Tuesday, July 25, 2023
Old Covenant vs New
As per Jeremiah 31, the difference between the old and new covenants is that the first consisted of God's law written on stone tablets, whereas the second consists of God's law written in the inward parts and in the heart.
Ten Commandments Nailed to the Cross?
It is a common misunderstanding that Col 2:14-17 says that the Ten Commandments were nailed to the cross. But that's not what Col 2:14-17 says. Because of the similarity in wording between this passage and the passage in Ephesians chapter 2, we need to look at both, starting with the Ephesians passage. But before getting to that, let's look at the word "ordinances".
The Word "Ordinances"
The word "ordinances" (dogmasin) in v. Col 2:14 is used in the "New Testament" five times, three of which clearly refer to human-uttered decrees, not God-uttered ones, and two of them being found here and in the sister passage of Eph 2:15. In the Greek Septuagint version of the "Old Testament", this word is only used in the book of Daniel, and only to refer to human decrees, not decrees from God. (A different word, dikaioma, is used ten times in the NT, and refers to God-uttered decrees and righteousness.) Paul knew his Greek, and he knew his Greek scriptures, and it seems odd that he would use the word "ordinances" to refer to God-uttered ordinances when it seems to only be used every where else in the scriptures for human-uttered ordinances.
The Ephesians Passage
WEB Eph 2:11 Therefore remember that once you, the Gentiles in the flesh, who are called “uncircumcision” by that which is called “circumcision” (in the flesh, made by hands), 12 that you were at that time separate from Christ, alienated from the commonwealth of Israel, and strangers from the covenants of the promise, having no hope and without God in the world. 13 But now in Christ Jesus you who once were far off are made near in the blood of Christ. 14 For he is our peace, who made both one, and broke down the middle wall of separation, 15 having abolished in his flesh the hostility, the law of commandments contained in ordinances, that he might create in himself one new man of the two, making peace, 16 and might reconcile them both in one body to God through the cross, having killed the hostility through it. 17 He came and preached peace to you who were far off and to those who were near. 18 For through him we both have our access in one Spirit to the Father. 19 So then you are no longer strangers and foreigners, but you are fellow citizens with the saints and of the household of God, 20 being built on the foundation of the apostles and prophets, Christ Jesus himself being the chief cornerstone; 21 in whom the whole building, fitted together, grows into a holy temple in the Lord; 22 in whom you also are built together for a habitation of God in the Spirit.
If "ordinances" refers to human-uttered decrees rather than God-uttered ones, then Paul is saying here that whatever is abolished is contained in human-uttered decrees.
Whatever was abolished was abolished for the purpose of making one new man out of the two (Jew and Gentile), as well as for the purpose of reconciling that one new man with God. So whatever the hostility is that was abolished, it kept a middle wall of separation between Jew and Gentile. The God-uttered law of Moses (and/or the Ten Commandments) did not create this separation; in fact, the law repeatedly enjoins the Jew to treat the non-Jew as one of their own.
However, the human-uttered Oral Law of the scribes and Pharisees did create this wall between Jew and non-Jew. Some of their ancient documents refer to this wall as a "fence", and interestingly enough, in the Greek of the phrase "middle wall of separation" is the word for "fence'. When Peter visited the non-Jew, God-fearing Cornelius, he opened the discussion by saying it was not lawful for a Jew to visit a non-Jew, but God had made him to understand that God did not agree with this "law"; this law can not be found in the God-uttered scriptures, but it can be found in the human-uttered Oral Law of the ancient Pharisees.
The "law of commandments contained in ordinances" phrase seems to refer to human-uttered Oral Law that separated Gentile from Jew. God's law never made this separation, but the Pharisees with their traditions did make the separation.
By drawing Gentile believers into the commonwealth of Israel, and making them part of the Israelite family of God, this "fence" no longer applied to them. The Oral Law, with its fence, it's middle wall of separation, is untouched, but the people whom it would have affected is no longer affected by it, because they are no longer foreigners.
So in effect, the division, the hostility, the separation, enforced by these human-uttered ordinances was nailed to the cross, breaking down the middle wall of separation between Jew and Gentile. It was not a law, either the Oral Law or the Ten Commandments law, which was nailed to the cross, but rather the division between Jew and non-Jew that was nailed to the cross. This nailing is about peace between Jew and non-Jew, and then peace between that united new man and God; it is not about doing away with a legal system. On the cross, Jesus bore our sins against a legal system, not the legal system itself.
The Colossians Passage
WEB Col 2:11 In him you were also circumcised with a circumcision not made with hands, in the putting off of the body of the sins of the flesh, in the circumcision of Christ, 12 having been buried with him in baptism, in which you were also raised with him through faith in the working of God, who raised him from the dead. 13 You were dead through your trespasses and the uncircumcision of your flesh. He made you alive together with him, having forgiven us all our trespasses, 14 wiping out the handwriting in ordinances which was against us. He has taken it out of the way, nailing it to the cross. 15 Having stripped the principalities and the powers, he made a show of them openly, triumphing over them in it.
The phrase here in Colossians is similar to the phrase in Ephesians, but it turns out to be an ancient Jewish idiom. As many Bible translations now render it, it refers to a "criminal record", a list of the laws we have broken, an "IOU", or a "mortgage debt". The Phillips version, while not at all a literal rendering, colors the meaning in quite nicely: "Christ has utterly wiped out the damning evidence of broken laws and commandments which always hung over our heads, and has completely annulled it by nailing it over his own head on the cross".
Conclusion
This passage, Col 2:14-17, is not telling us that the Ten Commandments have been nailed to the cross. It's telling us that that the legal charges against us in a court of law, which normally would be nailed above the head of the person being executed on a cross (as Jesus' "charges" were nailed above his head on his cross - Luke 23:38), were nailed above Jesus' head instead of ours, freeing us from the charges and placing them on him.
The sister passage in Eph 2, also is not telling us that the Ten Commandments have been nailed to the cross. It's telling us that non-Jewish believers in the Messiah are just as Jewish as the native Jews, and that the separation between the two, as contained in human-uttered oral traditions, has been nailed to the cross.
Originally published at: https://kentwest.blogspot.com/2023/07/ten-commandments-nailed-to-cross.html
Friday, June 16, 2023
Friday, May 05, 2023
A Simple Introduction to CLAP in the Rust Programming Language
"CLAP" stands for Command-Line Argument Parser.
In order to get started using clap, you need to understand three things:
1 - How to Run a Simple Rust Program
Suppose you want to create a program called "fun". You should have a working installation of Rust and Cargo (the web is full of explanations of how to install this). In your file directory where you want to create the "fun" project:
$ cargo new fun
$ cargo run
This should print "Hello, World!", because every cargo-created project has this simple capability as a starting point.
2 - How to Work with a struct
This is a little bit more advanced. Here's the "src/main.rs" program from our "cargo new fun" command above:
fn main()
println!("Hello, world!");
}
A structure is simply a variable of a customized type, which often holds other variables. You should be familiar in a general way with variables, and know that they come in various kinds, or types: there are String variables and there are i32 variables and there are usize variables, etc. A struct simply allows you to create your own customized variable type, and fill it with a collection of other variables of various types.
Say you have a variable called "last" which holds a person's last name, and a variable called "age" which holds that person's age in years. The first variable might be a type of String (which holds a string of text), and the second might be a type of i8 (which holds an integer in the range of -128 to +127; a u8, which holds a range of 0 to 255, might be a better option).
Think of these two variables as two different types of fruit. We can carry them around in our hands if we like, or we can create a shopping bag to put them into. This shopping bag is analogous to a struct, except that a struct is well-defined as to what it can hold, whereas a shopping bag will hold just about anything pretty much, willy-nilly.
So a better way to think of it is as a custom-tool case, with exact-fitting compartments for the tools.
Example:
// This is the "master design" from which all toolboxes will be built.
struct toolbox_template {
owner: String, // It will belong to a specific person,
hammer: String, // and will hold a hammer,
screwdriver: i8, // and a screwdriver of a certain size.
}
Note that this is just the definition of the "template" for a toolbox; it doesn't actually create a toolbox. Let's put this template into our "main.rs" file, along with the creation of two toolboxes based on this template, and then we'll print out a couple of messages about those tools. (We no longer need the "Hello, World!" println, so we'll delete it as per the Strikethru marking.)
struct ToolboxTemplate {
owner: String,
hammer: String,
screwdriver: i8,
}
fn main() {
println!("Hello, world!");
// Let's build a custom toolbox for Him.
let his_box: ToolboxTemplate = ToolboxTemplate {
owner: "Joe".to_string(),
hammer: "sledge".to_string(),
screwdriver: 8,
};
// And one for Her.
let her_box: ToolboxTemplate = ToolboxTemplate {
owner: "Jane".to_string(),
hammer: "pink-handled".to_string(),
screwdriver: 4
};
// Print the details of his toolbox.
println!("{} has a {} hammer and a Number {} screwdriver.",
his_box.owner, his_box.hammer, his_box.screwdriver
);
// And of hers.
println!("{} has a {} hammer and a Number {} screwdriver.",
her_box.owner, her_box.hammer, her_box.screwdriver
);
} // end of main()
So you can see that a struct is just a custom-built variable, a "carrying case", that holds various other variables. The "struct" part defines the type, and then you have to create variables of that type which actually hold the desired data.
A Basic clap Setup
So, you can write a simple Rust program, and you kindda understand a struct. Good. Now lets add clap into the mix.
We first have to tell Cargo (the Rust "compiler" (sort of, but not really)) about Clap. This is done by adding some information to the "Cargo.toml" file. Before doing this, my Cargo.toml file for the "fun" program looks like this:
$ cat Cargo.toml [package] name = "fun" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] $
If you have a recent version of Clap/Cargo, you can add Clap to your "fun" project with this command:
$ cargo add clap --features derive
That'll generate some churn, after which your "Cargo.toml" file will look more like this (the new stuff is in hilite):
$ cat Cargo.toml [package] name = "fun" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies]clap = { version = "4.2.7", features = ["derive"] }
Now Cargo knows about clap. Your program does not, but Cargo does. When you compile your program again, Cargo will include all the clap stuff. Try it. Your program will run just as it did before, except for all the extra compiling of Clap.
We want to be able to specify our hammer and screwdriver on the command-line. The command will look something like this:
$ code run -- --name=Bob --hammer=peen --screwdriver=9
Add the Strikethru. The "use" line simply tells our program the path to find parse-related stuff in the Clap crate. The "derive" line magically makes our struct definition able to associate its components with information coming from the Command-Line Argument Parser (clap).
use clap::Parser; #[derive(Parser)] struct ToolboxTemplate { owner: String, hammer: String, screwdriver: i8, } fn main() {// Let's build a custom toolbox for Him. let his_box: ToolboxTemplate = ToolboxTemplate { owner: "Joe".to_string(), hammer: "sledge".to_string(), screwdriver: 8, }; // And one for Her. let her_box: ToolboxTemplate = ToolboxTemplate { owner: "Jane".to_string(), hammer: "pink-handled".to_string(), screwdriver: 4 }; println!("{} has a {} hammer and a Number {} screwdriver.", his_box.owner, his_box.hammer, his_box.screwdriver ); // Print the details of his toolbox. println!("{} has a {} hammer and a Number {} screwdriver.", his_box.owner, his_box.hammer, his_box.screwdriver );// Let's build a toolbox based on command-line arguments. let toolbox: ToolboxTemplate = ToolboxTemplate::parse(); // And of hers.// And then print out the details of that toolbox. println!("{} has a {} hammer and a Number {} screwdriver.",her_box.owner, her_box.hammer, her_box.screwdrivertoolbox.owner, toolbox.hammer, toolbox.screwdriver ); } // end of main()
If you try to run this with arguments, like so:
$ cargo run -- --name=Bob hammer=peen screwdriver=9
you'll get a message about an unexpected argument, and a tip that tells you to do what you're already doing, along with a "usage" blurb.
However, if you run your program this way:
$ cargo run -- --Bob peen 9
it works!
And that's all that's needed for a very basic clap setup. It's reading your command-line arguments according to the position they're given. If you change the order, like this:
$ cargo run -- --9 Bob peen
you'll break your program. Like this, though:
$ cargo run -- --peen Bob 9
and you'll just get unwanted results.
So we want to be able to name our arguments. That's done like this:
use clap::Parser;
#[derive(Parser)]
struct ToolboxTemplate {
#[arg(long)]
owner: String,
#[arg(long)]
hammer: String,
#[arg(long)]
screwdriver: i8,
};
...
Try running that with $ cargo run -- --name=Bob hammer=peen screwdriver=9.
Hmm, a different error message. And again, it doesn't really make sense. Ah, but now I see it. We defined an argument named "owner", but we're typing in an argument named "name". Let's try this:
$ cargo run -- --owner name=Bob hammer=peen screwdriver=9.
Yay! That works!
But what if we really want to type in "name" instead of "owner", but don't want to change the variable name? Easy. Just do this:
use clap::Parser;
#[derive(Parser)]
struct ToolboxTemplate {
#[arg(long="name" )]
owner: String,
#[arg(long)]
hammer: String,
#[arg(long)]
screwdriver: i8,
};
...
What if we want to just use "s" for "screwdriver"?
use clap::Parser;
#[derive(Parser)]
struct ToolboxTemplate {
#[arg(long="name")]
owner: String,
#[arg(long)]
hammer: String,
#[arg(longshort )]
screwdriver: i8,
};
...
Then our command-line would like look: cargo run -- --name=Bob --hammer=peen -s=9 Notice that single-letter arguments are introduced with a single-hyphen (e.g. -s=9), rather than a double-hyphen (--s=9).
What if we want to just use "d" for "screwDriver"?
use clap::Parser;
#[derive(Parser)]
struct ToolboxTemplate {
#[arg(long="name")]
owner: String,
#[arg(long)]
hammer: String,
#[arg(short='d' )]
screwdriver: i8,
};
...
Notice that the definition uses single-quotes around "d" rather than double-quotes.
What if we want to allow either a short form or a long form?
use clap::Parser;
#[derive(Parser)]
struct ToolboxTemplate {
#[arg(short, long="name")]
owner: String,
#[arg(short, long)]
hammer: String,
#[arg(short='d',long )]
screwdriver: i8,
};
...
What if we want to make the name optional, with a default?
use clap::Parser;
#[derive(Parser)]
struct ToolboxTemplate {
#[arg(short,long="name",default_value_t=String::from("Bubba") )]
owner: String,
#[arg(short,long)]
hammer: String,
#[arg(short,long)]
screwdriver: i8,
};
...
Note that "Bubba".to_string() won't work in this case, so we had to use an alternative method of converting a string literal to a String-type. Don't worry for now about understanding that; just know that usually it doesn't matter which conversion method you use, and that if one method doesn't work, try another.
This short tutorial won't answer all your questions, but it should get you started. Have fun, Rustacean!
OCR On Your Smart Phone
Did you know that your phone can possibly do Optical Character Recognition (OCR)?
Mine, a Samsung Galaxy S22 Ultra, can.
- I took a picture of an informative display hanging on the wall, that had two columns of side-by-side text.
- I then used the camera's photo editing software (the pen icon at the bottom of the image below) to crop the image to just one of the text columns, and saved (upper-right corner of the cropped image below) that image.
- Then at the bottom right corner of that same editing window is a little yellow "T" in a broken-outline box (see the Gallery image above). When I clicked on that "T", it OCR'd the text and highlighted it.
- I was then able to single-press on the text, which popped up a menu allowing me to "Select All", which popped up another menu allowing me to "Copy".
- I could then go to an editor of some sort, and "Paste" the text into the editor.
- I then went back to my image, and edited it again, and "Revert"ed it back to the original.
- I then repeated the process for the second column.
In just a minute or two, I had the full text of the two columns of the informative display in an editor. With a clean original image with clean-looking text, the accuracy is very high.
The first unit of the hospital was erected in September 1924, at a cost of $150,000. West Texas Baptist Sanitarium had five stories, 72 rooms and admitted more than 800 patients during the first year.
When it opened, West Texas Baptist Sanitarium touted: hot and cold running water in each room; excellent nursing services; three modern elevators; three well-equipped operating rooms; capable physicians and surgeons; and an obstetrical department.
Labor and delivery services were quickly utilized. The first baby was born at Hendrick less than one month after the doors opened. Pauline Marie Turnidge, daughter of Mr. and Mrs. W.A. Turnidge, was born on October 17, 1924.
The vision of a hospital for the Texas Midwest was well under way as the vision of Reverend Millard Jenkins became a reality. The motto for West Texas Baptist Sanitarium was that it opened its doors to everyone, "no matter what your belief or creed."
Friday, February 03, 2023
Using Clap to Parse Rust Program Arguments
The native Rust argument-parsing capability is pretty limited, so we are now turning to the third-party crate, "Clap" (which stands for Command-Line Argument Parser). To do that, let's create a new project:
$ cd ~/projects/RUST $ cargo new parse_clap $ cd parse_clap
You should pretty much know what the "src/main.rs" file looks like in a new Cargo-created Rust project. Before we begin working with that file, we need to let Cargo know we're going to use the "Clap" crate. Since this is a new project, the "Cargo.toml" file has no dependencies listed. We'll need to add a dependency to the "Cargo.toml" file for Clap.
Clap is found at "crates.io" (a Rust-maintained web site for all things Rustacean). If you web-browse to that site, you can search for "clap", and you'll find (at least near the time of this writing) both a version 3 and a version 4. We want the most recent version, which at the time of this writing is 4.0.29. If you'll click on it to get more details, you'll see in the right-hand column a line that needs to be added to your "Cargo.toml" file, specifically to the "[dependencies]" section, in order to tell Cargo how to use the Clap crate. Up until very recent versions of Cargo, this had to be added to the file manually, but with more recent versions of Cargo, you can just run:
$ cargo add clap@4.0.29
If you don't want a specific version, but rather prefer the newest one available, you can instead run:
$ cargo add clap
Be aware, this may take a few minutes. If you do this, and then examine your "Cargo.toml" file again, you'll discover that the needed line has automatically been added to that file.
But, unfortunately, this instruction does not tell you that you need to add more to that command, in order to get all that we need. You can learn this by clicking on the "docs.rs" link at the "crates.io" website. The command you really need (you can run it even if you ran the previous command) is:
$ cargo add clap --features derive
The second run goes much faster than the first, because most of the work has already been done in our first attempt to add this to "Cargo.toml".
Now look again at your "Cargo.toml" file; if it looks pretty much like below, we should be good to go.
[package]
name = "parse_clap"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.0.29", features = ["derive"] }
Now we're ready to edit your "src/main.rs" file to be identical to the following:
use clap::Parser;
#[derive(Parser)]
struct ArgsType {
/// Are you happy or sad?
#[arg(short, long)]
mood: String,
}
fn main() {
let switches: ArgsType = ArgsType::parse();
if switches.mood == "happy"
{ println!("Whoo-hoo! I am {}! {}! {}! {}!", switches.mood, switches.mood, switches.mood, switches.mood) };
if switches.mood == "sad"
{ println!("Boo-hoo! I am {}!", switches.mood) };
println!("Hello, world!");
} // end of main()
Now compile and run this with the indicated program arguments, like so:
cargo run -- --mood happy
Try providing different switches, in different order, in different numbers. Try the "-V" option, as well as the "-v" non-option. Try the "-h" option.
You can see that this Clap crate is already pretty useful, in that it provides some help screens when the arguments are not what are expected. It doesn't handle every wrong argument (as the program is currently written), but you can see that there's some potential here.
Let's try to understand this program, and then explore a bit of that potential.
The first line, "use clap::Parser;" preps the system for the other two "Parse"-related statements. Just know it's needed.
The next line tells the system that the parser will be deriving its arguments from the struct we build next. Clap can be configured using the Builder Application Programming Interface (API) or the Derive API (or a mix, as I understand it). As a general rule, unless you need to get deeper under the hood of Clap, you'll probably want to use the Derive API. The FAQ at https://docs.rs/clap/4.1.4/clap/_faq/index.html#when-should-i-use-the-builder-vs-derive-apis says this:
When should I use the builder vs derive APIs?
Our default answer is to use the Derive API:
- Easier to read, write, and modify
- Easier to keep the argument declaration and reading of argument in sync
- Easier to reuse, e.g. clap-verbosity-flag
The Builder API is a lower-level API that someone might want to use for
- Faster compile times if you aren’t already using other procedural macros
- More flexibility, e.g. you can look up an arguments values, their ordering with other arguments, and what set them. The Derive API can only report values and not indices of or other data.
So in other words, do it the way we're doing here, with a struct, not the way other tutorials might show you, without a struct. At least until you want/need to dive deeper.
The "struct" section provides a defining template for a new type of variable. This section does not declare an actual variable (we'll declare that later), but only a new type of variable. This new type of variable is based on a struct format. (A struct is a custom-made variable that holds other variables.)
Any variables declared to be of this new type are defined by this "struct' section, which defines what arguments are allowed to be given as the program's command-line arguments, and what the internal variable names are that will hold those arguments for use in the program. The Clap Derive API uses this struct type of structure to define and build this new type of variable. We could call this new type of variable anything we wanted, like "progInputs" or "options" or "OptionsType", etc. We're calling it "ArgsType". Currently this new type of variable defines one internal variable, named "mood", which is designated to hold String data.
The section that defines this inner "mood" variable is introduced by a line with three forward slashes (///).
Whereas two slashes are the beginning of a "comment", which is ignored
by the compiler but helps the programmer to keep notes about the code, a
three-slash line functions as both a comment and a documentation line,
which can be used by the compiler and by Clap to create help text. If
you run cargo run -- --help, you can see that text, "Are you happy or sad?", in the output.
The line beginning with a hash mark tells Clap how to handle this program argument: whether it can be entered as a long form (--mood), or as a short form (-m), or must it be required, or should it have a default value, etc.
We can add additional internal variables (and therefore additional program input possibilities) by adding more "#[arg..." sections to the struct design. For example, in addition to the user's mood, perhaps we'd like to know the person's name and age:
struct ArgsType {
/// Are you happy or sad?
#[arg(short, long)]
mood: String,
/// What is your name?
#[arg(short, long)]
name: String,
/// What is your age?
#[arg(short, long, default_value_t = 16)]
age: u8,
}
Notice that the "age" variable has a default value (which is a bit silly, but this is just an example). Because of this, Clap won't require the user to enter that option, but it will the other two. You can force it to be required like this:
#[arg(short, long, default_value_t = 3), required(true)]
but that kind of defeats the purpose of having a default.
Although
technically an age entered on the command line in a command such as cargo run -- --name Kent --age 35
starts out as a "String" (everything entered on the command line starts
out as a "String"), by the time it gets to our "age" variable, Clap
will have converted it from a "String" to a "u8" (which is an unsigned (i.e., positive) integer in the range of 0 to 255).
Note again that we have not yet declared a variable of this new type; we have only defined a new type of variable. We actually declare a variable in the main() function. Note also that since the struct is defined outside of the main() function, this definition of a new type of variable is "visible" (or "is in scope") to all parts of the program within this "main.rs" file. If we should create a new function later on in this same file, say, a function called "part_two()", that function will be able to access this "ArgsType" definition; had we put this definition within the main() function, it would only be visible to the main() function itself, but not to the "part_two()" function.
Now let's look at the main() function. The let switches: ArgsType = ArgsType::parse();
line actually defines our variable. The name of the variable is
"switches", and the type of the variable is, not String and not i32 and not u8 or
etc, but "ArgsType", the type we just invented. If this line seems complicated to you, take out the ": ArgsType", to make the line be just let switches = ArgsType::parse(); which may be less daunting to look at and therefore less daunting to understand. It's basically just calling a "function" named "parse" that is "located" in the "ArgsType" struct we just built (not exactly, but close enough), and assigning the results of that "function" to the variable "switches".
The variable "switches" now holds three variables within it (assuming three options are given as program inputs), which we can access as "switches.mood", "switches.name", and "switches.age". Here are some mods to our program, including a boolean flag to specify if the user is human or not, which defaults to "no":
use clap::Parser;
#[derive(Parser)]
struct ArgsType {
/// Are you happy or sad?
#[arg(short, long)]
mood: String,
/// What is your name?
#[arg(short, long, value_name = "What yo momma called you...")]
name: String,
/// What is your age?
#[arg(short, long, default_value_t = 16)]
age: u8,
/// Are you a human?
#[arg(short = 'H', long, default_value_t = false)] // 'h' would have conflicted with "help".
human: bool,
}
fn main() {
let switches: ArgsType = ArgsType::parse();
if switches.human {
println!("Hi, {}! You seem very {} to be {} years old, but that's understandable, since you are a human.",
switches.name,
switches.mood,
switches.age
);
} else {
println!("Hi, {}! You seem very {} to be {} years old, but that's understandable, since you are not a human.",
switches.name,
switches.mood,
switches.age
);
}
if switches.mood == "happy"
{ println!("Whoo-hoo! I am {}! {}! {}! {}!", switches.mood, switches.mood, switches.mood, switches.mood) };
if switches.mood == "sad"
{ println!("Boo-hoo! I am {}!", switches.mood) };
} // end of main()
Running this program results in:
$ cargo run -- --mood happy --name Kent --age 253
Compiling parse_clap v0.1.0 (/home/westk/projects/RUST/parse_clap)
Finished dev [unoptimized + debuginfo] target(s) in 0.56s
Running `target/debug/parse_clap --mood happy --name Kent --age 253`
Hi, Kent! You seem very happy to be 253 years old.
Whoo-hoo! I am happy! happy! happy! happy!
$
$ cargo run -- --name=Kent --age 253 -m happy --human -h
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/parse_clap --name=Kent --age 253 -m happy --human -h`
Usage: parse_clap [OPTIONS] --mood <MOOD> --name <What yo momma called you...>
Options:
-m, --mood <MOOD> Are you happy or sad?
-n, --name <What yo momma called you...> What is your name?
-a, --age <AGE> What is your age? [default: 16]
-H, --human Are you a human?
-h, --help Print help information
-V, --version Print version information
$
Note also that various formats can be used for entering the arguments:
--name Kent -nKent -n=Kent --name=Kent But --nameKent won't work.
And that's pretty much it. We've got our feet wet with parsing arguments in Rust using the Clap crate.
Monday, January 09, 2023
A Man's Most Important Relationships
The most important relationship a man has is with his God.
But the relationship he should focus most on is that with his wife.
God doesn't need a man's attention. A wife does.
Men, your wife likely has two fundamental needs that you need to meet:
- the need to feel secure (finances seem to you like a huge part of this, but there are bigger security issues for her)
- the need to feel valued (and listening to her and considering her viewpoint is a huge part of this)
Focus on these things. Make her feel valued; make her feel safe.
Monday, October 31, 2022
Notes About PowerShell: Adding a TreeView to the GUI - Part 3
In Notes About PowerShell: Adding a TreeView to the GUI - Part 2 of this series, we had a complete set of four PowerShell scripts that together creates a GUI window that displays a Windows Forms TreeView, allowing the user to select a family member from a small family tree. In this post, we're going to replace that family tree with the computer's file system.
Let's make one quick modification to tinker.ps1 file so that our results are graphically displayed in addition to textually in the console. Add in the bolded code below:
...
If ($Win.DialogResult -eq "OK") {
Write-Host("The OK button was pressed. The data retrieved are:")
Write-Host("`t Node: `"$($Node.Text)`".")
Write-Host("`tPath to the Node: `"$($NodePath.Text)`".")
[System.Windows.Forms.MessageBox]::Show("Results:`n`nNODE:`n `"$($Node.Text)`"`n`nPATH TO NODE:`n `"$($NodePath.Text)`"")
...
Give 'er a test spin.
Okay, on to putting the filesystem into a treeview. First, we'll need to have the drive letters of Windows. For experimentation/learning purposes, at a PowerShell prompt (not in your script), enter the following command, and you'll see results similar to the following:
PS C:\Users\acutech> Get-PSDrive Name Used (GB) Free (GB) Provider Root CurrentLocation ---- --------- --------- -------- ---- --------------- Alias Alias C 24.30 55.00 FileSystem C:\ Users\acutech Cert Certificate \ D FileSystem D:\ Env Environment Function Function HKCU Registry HKEY_CURRENT_USER HKLM Registry HKEY_LOCAL_MACHINE Variable Variable WSMan WSMan PS C:\Users\acutech>
We're only interested in the filesystem drive letters, not the registry keys or certificates or etc. So let's put some limitations on the command:
PS C:\Users\acutech> Get-PSDrive -PSProvider FileSystem Name Used (GB) Free (GB) Provider Root CurrentLocation ---- --------- --------- -------- ---- --------------- C 24.30 55.00 FileSystem C:\ Users\acutech D FileSystem D:\ PS C:\Users\acutech>
Better. But all we really want is the drive letter itself:
PS C:\Users\acutech> (Get-PSDrive -PSProvider FileSystem).Root C:\ D:\ PS C:\Users\acutech> (Get-PSDrive -PSProvider FileSystem).Name C D
Great! Both of these commands give us an array containing the filesystem drives. Let's load them up into the treeview.
Let's start with a reminder of our tinker_add_nodes.ps1 file:
$TreeView.Nodes.Add("John")
$TreeView.Nodes[0].Nodes.Add("Mary")
$TreeView.Nodes[0].Nodes.Add("Fred")
$TreeView.Nodes[0].Nodes[1].Nodes.Add("Delbert")
$TreeView.Nodes[0].Nodes.Add("Alvin")
$TreeView.Nodes.Add("William")
$TreeView.Nodes[1].Nodes.Add("Estelle")
$TreeView.Nodes[1].Nodes.Add("Angus")
$TreeView.Nodes[1].Nodes.Add("Eugene")
$TreeView.Nodes[1].Nodes.Add("Marvin")
$TreeView.SelectedNode = $TreeView.Nodes[0].Nodes[1].Nodes[0]
$Win.ActiveControl = $TreeView
$Node.Text = $TreeView.SelectedNode.Text
Delete all of that code; we're done with it. And replace it with this code:
$Drives = (Get-PSDrive -PSProvider FileSystem).Root # Get the drive letters in an array named $Drives
foreach ($_ in $Drives) { # For each drive letter in the array,
$TreeView.Nodes.Add($_) # add the drive letter to the treeview.
}
Open up .tinker.ps1 if you haven't already done so, and run it. You should get something like this:
This Document is Under Construction
Friday, October 28, 2022
Notes About PowerShell: Adding a TreeView to the GUI - Part 2
See Part 1 here.
If you just want to see the code, without reading how we get there, the code listings are at the bottom of this page.
In Part 1, we built a basic Windows form with a blank treeview. Now we're going to populate the treeview. Probably the three most popular "databases" that are looked at in PowerShell in a tree form are the file system, the Windows Registry, and Active Directory domains. I plan to look at each of these three sources via a treeview, but let's start with something simpler, a simple family tree.
As you remember from Part 1, most of our GUI-window construction takes place in an external file, "tinker_GUI.ps1", which is dot-sourced from our primary script file, "tinker.ps1". That primary script file currently looks like this:
# Offload GUI-building into a different file to decrease clutter here.
. "$PSScriptRoot\tinker_GUI.ps1"
# The computer is now looping forever waiting for user input.
# Once the input is the OK button or the Window's X (or
# keyboard equivalent), the window closes and the code below runs.
If ($Win.DialogResult -eq "OK") {
Write-Host("The OK button was pressed. The name in the box is `"$($NameBox.Text)`".")
} elseif ($Win.DialogResult -eq "Cancel") {
Write-Host("The X was pressed. I`'m not going to tell you the name that is in the box.")
} # end of If
If you've followed along with Part 1, you should be able to run the above script, and see a window with an empty treeview, a box for entering your name, and an OK button.
Since a treeview looks at data that is in a tree-like form, we're probably less interested in the user's name and more interested in the leaf-node endpoint of interest in our tree, and the path it takes to get to that leaf-node. So let's edit the second, external script file, so that it replaces the "Name" box with a "Node" box. Let's also take out the pre-fill of that box, and the activation/selection/focus of that box.
Use your PowerShell ISE to open the "tinker_GUI.ps1" script file, and make the following modifications, deleting the strike-out text and adding in the bolded text. You'll find it easier to change "NameBox" to "Node" by using the PowerShell ISE's Edit / Replace in Script... feature to replace all instances of "NameBox" with "Node".
...
$Win.AcceptButton = $buttonOK # Click = "Close form; I accept it as it now is."
# A textbox.The node.
$NameBoxNode = New-Object "System.Windows.Forms.Textbox"
$NameBoxNode.Size = "175,25"
$NameBoxNode.Location = "290,40"
$NameBox.Text = "Babushka" # Pre-fill the box.
$Win.Controls.Add($NameBoxNode)
$Win.ActiveControl = $NameBox # Once form shows, select this box.
# Put a label with the box.
$Label_NameBoxNode = New-Object 'System.Windows.Forms.Label'
$Label_NameBoxNode.Text = "Enter your nameSelected Node:"
$Label_NameBoxNode.Size = "150, 25"
$Label_NameBoxNode.Location = '290, 20'
$Win.Controls.Add($Label_NameBoxNode)
...
Try running your script, to make sure it all works as expected.
We're also interested in the path to that node, so let's add a "Path to Node" box (by adding the bolded text in the indicated place).
...
$Label_Node.Location = '290, 20'
$Win.Controls.Add($Label_Node)
# Path to the node.
$NodePath = New-Object "System.Windows.Forms.Textbox"
$NodePath.Size = "175,25"
$NodePath.Location = "290,120"
$Win.Controls.Add($NodePath)
# Put a label with the box.
$Label_NodePath = New-Object 'System.Windows.Forms.Label'
$Label_NodePath.Text = "Path to Node:"
$Label_NodePath.Size = "150, 25"
$Label_NodePath.Location = '290, 100'
$Win.Controls.Add($Label_NodePath)
# The TreeView object.
$TreeView = New-Object System.Windows.Forms.TreeView
...
Now, for our family tree.
Imagine two brothers, John and William. John has three kids, Mary, Fred, and Alvin, and Fred has one, Delbert. William has four, Estelle, Angus, Eugene, and Marvin. Let's make a tree that views these relationships, and put that tree into the treeview object on our GUI form.
Ideally, since adding things to the treeview is conceptually different than actually building the window and the node and path and treeview box forms, we'd put the coding instructions to add things to the treeview somewhere else besides in this file. However, once the form is shown on-screen (with the $Win.ShowDialog(), no code below that line is executed until that window form is closed.
So, our options are to embed the code in the midst of all this other code, or to put the code to an external file and then import it as a dot-sourced file into a location just above the $Win.ShowDialog() line, or to put the code into a function and call that function just prior to the $Win.ShowDialog() line.
Just for sake of example, let's add a couple of nodes, the two patriarch brothers, with the code embedded in the tinker_GUI.ps1 file:
...
$Win.Controls.Add($Label_TreeView) # Add the label to the form.
$TreeView.Nodes.Add("John")
$TreeView.nodes.Add("William")
# Display the form
$Win.ShowDialog()
...
Run the script, and you should start seeing the treeview come together.
Now, instead of embedding this code in the tinker_GUI.ps1 file, let's move that code out of it, and into a new file we can name tinker_add_nodes.ps1. So tinker_GUI.ps1 becomes:
... $Win.Controls.Add($Label_TreeView) # Add the label to the form.$TreeView.Nodes.Add("John") $TreeView.nodes.Add("William"). "$PSScriptRoot\tinker_add_nodes.ps1" # Import file with code to populate tree. # Display the form $Win.ShowDialog() ...
and tinker_add_nodes.ps1 becomes:
$TreeView.Nodes.Add("John")
$TreeView.nodes.Add("William")
Running tinker.ps1 should still produce the output you expect.
Now let's add the kids and grandkid.
$TreeView.Nodes.Add("John")
$TreeView.Nodes[0].Nodes.Add("Mary")
$TreeView.Nodes[0].Nodes.Add("Fred")
$TreeView.Nodes[0].Nodes[1].Nodes.Add("Delbert")
$TreeView.Nodes[0].Nodes.Add("Alvin")
$TreeView.Nodes.Add("William")
$TreeView.Nodes[1].Nodes.Add("Estelle")
$TreeView.Nodes[1].Nodes.Add("Angus")
$TreeView.Nodes[1].Nodes.Add("Eugene")
$TreeView.Nodes[1].Nodes.Add("Marvin")
As you can see, the .Nodes values are arrays, that attach behind each other like the railroad cars of a train. The first level array, $Win.Nodes has two elements: .Nodes[0] contains the name "John". So by adding "Mary" and "Fred" as new element nodes to $Win.Node[0], "Mary" becomes $Win.Nodes[0].Nodes[0] and shows up in the treeview as "John's" daughter, as does "Fred" as $Win.Nodes[0].Nodes[1]. Don't worry too much if this doesn't yet make sense to you; it should start making more sense the farther we go and the more you work with it.
To help visualize this, we can add this code:
...
$TreeView.Nodes[1].Nodes.Add("Eugene")
$TreeView.Nodes[1].Nodes.Add("Marvin")
$TreeView.SelectedNode = $TreeView.Nodes[0].Nodes[1].Nodes[0]
...
You'll see that the tree has been expanded out to Delbert, without you having to expand anything using your mouse. If you press TAB a couple of times, until the focus lands on the treeview box, you'll see "Delbert" is highlighted.
We can even move the TAB focus to the treeview programatically:
...
$TreeView.Nodes[1].Nodes.Add("Eugene")
$TreeView.Nodes[1].Nodes.Add("Marvin")
$TreeView.SelectedNode = $TreeView.Nodes[0].Nodes[1].Nodes[0]
$Win.ActiveControl = $TreeView
...
You may recall we used the "$Win.ActiveControl" setting earlier, in the definitions for the "Node" box, but then deleted that. If it had not been deleted, this second instance would simpy overwrite the effects of the first instance, because it comes later in the code.
But speaking of the "Node" box, this'd be a great time to fill it with the selected node:
... $TreeView.SelectedNode = $TreeView.Nodes[0].Nodes[1].Nodes[0] $Win.ActiveControl = $TreeView $Node.Text = $TreeView.SelectedNode.Text
You might think that since this is a property of $Node, it should go with all the other property settings in the section of code where we first defined $Node. That's great thinking. Unfortunately, since PowerShell is an interpreted language rather than a compiled language, PowerShell doesn't know about the $TreeView.SelectedNode.Text until after it reads this section of code, long after it has passed that section of code. So this assignment needs to go here instead of there.
This would also be a good time to report the selected node after the OK button is pressed. Remember, this code is in the main file, tinker.ps1.
...
# The computer is now looping forever waiting for user input.
# Once the input is the OK button or the Window's X (or
# keyboard equivalent), the window closes and the code below runs.
If ($Win.DialogResult -eq "OK") {
Write-Host("The OK button was pressed. The namenode in the box is `"$($NameBoxNode.Text)`".")
} elseif If ($Win.DialogResult -eq "Cancel") {
Write-Host("The X was pressed. I`'m not going to tell you the namenode that is in the box.")
} # end of If
If you click around in the treeview, say, by clicking on "Mary", you'll see the focus follows your click. But nothing else happens.
In order to perform some action when we click in the treeview, we'll need more code. In the definition for the treeview, we'll add this line:
...
$TreeView.Size = "240,400"
$treeview.add_NodeMouseClick({Write-Host("Ouch")})
$Win.Controls.Add($TreeView)
...
All this does is print "Ouch" to the PowerShell console. (You could print it to a pop-up message box with a slightly more-complicated statement - [System.Windows.Forms.MessageBox]::Show("Ouch"). (The brackets are kind of a short-hand way of creating an object on-the-fly, without the whole "$MessageBox = New-Object..." variable declaration thingy, but since this is a run-time "declaration", the dot, ".", becomes "::" (among other differences).))
Each time you click anywhere in the treeview area, you'll get an "Ouch".
However, what if we want to do more than a single command on a mouse-click?
We could continue adding code in this embedded manner, like so:
$treeview.add_NodeMouseClick(
{
Write-Out("Ouch")})
Write-Host("You just clicked away from $($TreeView.SelectedNode).") # Need to fix: click on the same already-highlighted name, and this message will be inaccurate.
Write-Host("Stop it! That hurts!")
}
)
Or we could use a variable here that functions like a function by standing in for a whole section of code, or use an actual function call. To keep the various code pieces separate as much as we can, and keep one section of code uncluttered by another portion of code, let's put the variable/function in another file, say, tinker_node_selected.ps1.
$TreeViewMouseClickEvent = {
Write-Host("Ouch")
Write-Host("You just clicked away from $($TreeView.SelectedNode).") # Need to fix: click on the same already-highlighted name, and this message will be inaccurate.
Write-Host("Stop it! That hurts!")
}
And we'll have to make two changes to tinker_GUI.ps1:
# Initialize the PowerShell GUI Add-Type -AssemblyName System.Windows.Forms . "$PSScriptRoot\tinker_node_selected.ps1" ...$treeview.add_NodeMouseClick({Write-Host("Ouch")})$treeview.add_NodeMouseClick($TreeViewMouseClickEvent)
Or we can create a function, and call that function:
function TreeViewMouseClickEvent {
Write-Host("Ouch")
Write-Host("You just clicked away from $($TreeView.SelectedNode).") # Need to fix: click on the same already-highlighted name, and this message will be inaccurate.
Write-Host("Stop it! That hurts!")
} # end of TreeViewMouseClickEvent function
and
$treeview.add_NodeMouseClick({TreeViewMouseClickEvent})
Notice that with the embedded and function methods, curly braces are needed, but not with the variable method. Notice also that the function method is not preceded by the $ sign, whereas the variable method is. Lastly, note that the variable declaration includes an = sign, whereas the function declaration does not.
For a fuller look at these three methods, see here. We'll continue on using the function method.
If you watch the console as you click around in the treeview, you'll notice that the name that appears in the console is the name you leave, rather than the name on which you click. That's not the behavior we want. The reason this happens is because the PowerShell scripting engine runs the function/variable/embedded code before changing the treeview's selected node. We don't want to process the name we just left, but rather the name we just clicked on. So we'll have to change our function, to use arguments that the Add_NodeMouseClick feature automatically provides to us:
...Write-Host("You just clicked away from $($TreeView.SelectedNode).") # Need to fix: click on the same already-highlighted name, and this message will be inaccurate.Write-Host("You just clicked on $($_.Node.Text).") ...
The $_ represents the un-named variable given to our function which holds the arguments from the Add_NodeMouseClick event.
Those console messages aren't very valuable to us, though. Let's instead use this function to change out the text of the two text boxes on our form.
function TreeViewMouseClickEvent {
Write-Host("Ouch")
Write-Host("You just clicked on $($_.Node.Text).")
Write-Host("Stop it! That hurts!")
# Fill in the "Node" and Path: fields on the form, based on the node just selected.
$Node.Text = $_.Node.Text
$NodePath.Text = $_.Node.FullPath
} # end of TreeViewMouseClickEvent function
This almost works like we want, except you'll notice that if you click on a plus or minus in the treeview, the "Node:" and "Path:" fields change, even if we haven't actually selected a different node. Thinking about it, that makes sense; the Mouse-click event is raised when we click the mouse in the treeview area. We just need to use a different event. Easy. We'll also change the name of our function, to make it more accurate as to what is going on.
In tinker_node_selected.ps1:
...
function TreeViewMouseClickEvent {
function TreeNodeSelected {
...
} # end of TreeViewMouseClickEvent function
} # end of TreeNodeSelected function
And in tinker_GUI.ps1:
...$treeview.add_NodeMouseClick({TreeViewMouseClickEvent})$treeview.add_AfterSelect({TreeNodeSelected}) ...
Now we have a working form with a working treeview that allows us to read the selected node and its path. Let's change our output to include both of these bits of data. In tinker.ps1:
...If ($Win.DialogResult -eq "OK") { Write-Host("The OK button was pressed. The node in the box is `"$($Node.Text)`".")If ($Win.DialogResult -eq "OK") { Write-Host("The OK button was pressed. The data retrieved are:") Write-Host("`t Node: `"$($Node.Text)`".") Write-Host("`tPath to the Node: `"$($NodePath.Text)`".") ...
Just for clarity, here are the four files we are using, in their entirety:
tinker.ps1:
# Offload GUI-building into a different file to decrease clutter here.
. "$PSScriptRoot\tinker_GUI.ps1"
# The computer is now looping forever waiting for user input.
# Once the input is the OK button or the Window's X (or
# keyboard equivalent), the window closes and the code below runs.
If ($Win.DialogResult -eq "OK") {
Write-Host("The OK button was pressed. The data retrieved are:")
Write-Host("`t Node: `"$($Node.Text)`".")
Write-Host("`tPath to the Node: `"$($NodePath.Text)`".")
} elseif ($Win.DialogResult -eq "Cancel") {
Write-Host("The X was pressed. I`'m not going to tell you the node that is in the box.")
} # end of If
tinker_GUI.ps1:
# Initialize the PowerShell GUI
Add-Type -AssemblyName System.Windows.Forms
. "$PSScriptRoot\tinker_node_selected.ps1"
# Create a new window form.
$Win = New-Object System.Windows.Forms.Form
$Win.ClientSize = '490,460' # Size of window frame.
$Win.text = "My Parent Window" # Title of the Win frame.
$Win.BackColor = "#ffffff" # Background color of the Win frame.
# Create an OK button.
$buttonOK = New-Object 'System.Windows.Forms.Button' # Create the button.
$buttonOK.Text = "OK" # Puts text on the button.
$buttonOK.Location = '350, 240'
$buttonOK.Size = '60, 25'
$buttonOK.Anchor = 'Bottom, Right' # Keep button in relative bottom-right of Win.
$buttonOK.DialogResult = "OK" # "None|OK|Cancel|Abort|Retry|Ignore|Yes|No".
$Win.Controls.Add($buttonOK) # Add the button to the form.
$Win.AcceptButton = $buttonOK # Click = "Close form; I accept it as it now is."
# The node.
$Node = New-Object "System.Windows.Forms.Textbox"
$Node.Size = "175,25"
$Node.Location = "290,40"
$Win.Controls.Add($Node)
# Put a label with the box.
$Label_Node = New-Object 'System.Windows.Forms.Label'
$Label_Node.Text = "Selected Node:"
$Label_Node.Size = "150, 25"
$Label_Node.Location = '290, 20'
$Win.Controls.Add($Label_Node)
# Path to the node.
$NodePath = New-Object "System.Windows.Forms.Textbox"
$NodePath.Size = "175,25"
$NodePath.Location = "290,120"
$Win.Controls.Add($NodePath)
# Put a label with the box.
$Label_NodePath = New-Object 'System.Windows.Forms.Label'
$Label_NodePath.Text = "Path to Node:"
$Label_NodePath.Size = "150, 25"
$Label_NodePath.Location = '290, 100'
$Win.Controls.Add($Label_NodePath)
# The TreeView object.
$TreeView = New-Object System.Windows.Forms.TreeView
$TreeView.Location = "10,40"
$TreeView.Size = "250,400"
$treeview.add_AfterSelect({TreeNodeSelected})
$Win.Controls.Add($TreeView) # Add the tree view to the main window form.
# Put a label with the object.
$Label_TreeView = New-Object System.Windows.Forms.Label
$Label_TreeView.Location = "10,20"
$Label_TreeView.Size = "150,25"
$Label_TreeView.Text = "My Tree Object:"
$Win.Controls.Add($Label_TreeView) # Add the label to the form.
. "$PSScriptRoot\tinker_add_nodes.ps1" # Import file with code to populate tree.
# Display the form.
$Win.ShowDialog()
tinker_add_nodes.ps1:
$TreeView.Nodes.Add("John")
$TreeView.Nodes[0].Nodes.Add("Mary")
$TreeView.Nodes[0].Nodes.Add("Fred")
$TreeView.Nodes[0].Nodes[1].Nodes.Add("Delbert")
$TreeView.Nodes[0].Nodes.Add("Alvin")
$TreeView.Nodes.Add("William")
$TreeView.Nodes[1].Nodes.Add("Estelle")
$TreeView.Nodes[1].Nodes.Add("Angus")
$TreeView.Nodes[1].Nodes.Add("Eugene")
$TreeView.Nodes[1].Nodes.Add("Marvin")
$TreeView.SelectedNode = $TreeView.Nodes[0].Nodes[1].Nodes[0]
$Win.ActiveControl = $TreeView
$Node.Text = $TreeView.SelectedNode.Text
tinker_node_selected.ps1:
function TreeNodeSelected {
# Fill in the "Node" and Path: fields on the form, based on the node just selected.
$Node.Text = $_.Node.Text
$NodePath.Text = $_.Node.FullPath
} # end of TreeViewMouseClickEvent function
Next up, in Part 3, we'll do this using the file system.
Notes About PowerShell: Adding a TreeView to the GUI, Part 1
Previously we built the Simplest PowerShell GUI, and then added a few features to it here.
The script below is that last more complex GUI (with a few edits). It presents a window frame, with an OK button object, and a textbox object that asks for a name to be entered. We'll build on this script to add a treeview object. A treeview allows you to browse a tree-like structure, such as the file system, or an Active Directory domain, or the Windows Registry. So fire up the ol' PowerShell ISE, copy and paste in the following code (making sure the quotation marks don't turn into "smart" quotation marks), and then save the file as something like "tinker.ps1". Then try running it to make sure it works.
# Initialize the PowerShell GUI
Add-Type -AssemblyName System.Windows.Forms
# Create a new window form.
$Win = New-Object System.Windows.Forms.Form
$Win.ClientSize = '490,460' # Size of window frame.
$Win.text = "My Parent Window" # Title of the Win frame.
$Win.BackColor = "#ffffff" # Background color of the Win frame.
# Create an OK button.
$buttonOK = New-Object 'System.Windows.Forms.Button' # Create the button.
$buttonOK.Text = "OK" # Puts text on the button.
$buttonOK.Location = '350, 240'
$buttonOK.Size = '60, 25'
$buttonOK.Anchor = 'Bottom, Right' # Keep button in relative bottom-right of Win.
$buttonOK.DialogResult = "OK" # "None|OK|Cancel|Abort|Retry|Ignore|Yes|No".
$Win.Controls.Add($buttonOK) # Add the button to the form.
$Win.AcceptButton = $buttonOK # Click = "Close form; I accept it as it now is."
# A textbox.
$NameBox = New-Object "System.Windows.Forms.Textbox"
$NameBox.Size = "175,25"
$NameBox.Location = "290,40"
$NameBox.Text = "Babushka" # Pre-fill the box.
$Win.Controls.Add($NameBox)
$Win.ActiveControl = $NameBox # Once form shows, select this box.
# Put a label with the box.
$Label_NameBox = New-Object 'System.Windows.Forms.Label'
$Label_NameBox.Text = "Enter your name:"
$Label_NameBox.Size = "150, 25"
$Label_NameBox.Location = '290, 20'
$Win.Controls.Add($Label_NameBox)
# Display the form.
$Win.ShowDialog()
# The computer is now looping forever waiting for user input.
# Once the input is the OK button or the Window's X (or
# keyboard equivalent), the window closes and the code below runs.
If ($Win.DialogResult -eq "OK") {
Write-Host("The OK button was pressed. The name in the box is `"$($NameBox.Text)`".")
} elseif ($Win.DialogResult -eq "Cancel") {
Write-Host("The X was pressed. I`'m not going to tell you the name that is in the box.")
} # end of If
Now we're ready to create the empty treeview object. The text in bold below is what we're adding.
... # Put a label with the box. $Label_NameBox = New-Object 'System.Windows.Forms.Label' $Label_NameBox.Text = "Enter your name:" $Label_NameBox.Size = "175, 23" $Label_NameBox.Location = '10, 20' $Win.Controls.Add($Label_NameBox) # The TreeView object. $TreeView = New-Object System.Windows.Forms.TreeView $TreeView.Location = "10,40" $TreeView.Size = "250,400" $Win.Controls.Add($TreeView) # Add the tree view to the main window form. # Put a label with the object. $Label_TreeView = New-Object System.Windows.Forms.Label $Label_TreeView.Location = "10,20" $Label_TreeView.Size = "150,25" $Label_TreeView.Text = "My Tree Object:" $Win.Controls.Add($Label_TreeView) # Add the label to the form. # Display the form. $Win.ShowDialog() ...
That's a lot of code to be scrolling up and down in, and it's pretty easy to get lost in it. It'd be good if we could break up this big monolithic piece of code into smaller, more manageable chunks.
Most of this code is involved in the actual GUI-building, so the breaking up into chunks won't do a great deal for us at this point, but it will help to compartmentalize the different functional parts. We'll break this code into two pieces, and put them in separate files. On the one hand, that's a disadvantage, because now you're dealing with multiple files for your project, but on the other hand, it's an advantage, in that it breaks up the project into more manageable bite-size pieces.
So click on Edit / Select All in your PowerShell ISE, then Edit / Copy, to copy all the text into your Clipboard. Now click on click on File / New to open up a new blank tab, and then Edit / Paste to paste the script into a new tab. Go to the very bottom of the file and select everything after the "$Win.ShowDialog()" line, and then Edit / Cut that selection into the Clipboard. Now save this tab as something like "tinker_GUI.ps1".
You now have two almost identical files, one named "tinker.ps1" that has the entire script, and one named "tinker_GUI.ps1" that has everything except the last few lines.
Now go back to the "tinker.ps1" tab and with all the text still selected, Edit / Paste.
Now you have transferred all the GUI-building pieces of the script to an external file named "tinker_GUI.ps1", and left just a little bit of code in the original file, "tinker.ps1".
But the original "tinker.ps1" no longer knows anything about the code in the external "tinker_GUI.ps1" file, so we'll have to tell it about the external file. Here now is what the "tinker.ps1" file should look like, with code in bold that you need to add:
# Offload GUI-building into a different file to decrease clutter here.
. "$PSScriptRoot\tinker_GUI.ps1"
# The computer is now looping forever waiting for user input.
# Once the input is the OK button or the Window's X (or
# keyboard equivalent), the window closes and the code below runs.
If ($Win.DialogResult -eq "OK") {
Write-Host("The OK button was pressed. The name in the box is `"$($NameBox.Text)`".")
} elseif ($Win.DialogResult -eq "Cancel") {
Write-Host("The X was pressed. I`'m not going to tell you the name that is in the box.")
} # end of If
Save the file, and try running it. Note that the "tinker.ps1" tab must be the active tab to run the program. If the active tab is tinker_GUI.ps1, that portion of code will run, but the tinker.ps1 portion of code won't run. In this case, it really doesn't matter all that much. That won't always be the case, though; you need to run the tab that contains the "main" piece of code.
This dot-sourcing trick essentially tells the first script to go find the referenced file and paste that file's contents in at this dot-source location. When the script runs, that second file's contents replace this line, so it's just like all that second file's contents are in this file, but without the eyeball-clutter of having those contents actually in this file.
Now we can focus just on the mechanics of working with the tree. We'll look at that in "Notes About PowerShell: Adding a TreeView to the GUI - Part 2".

